简介
本 系列 文章向您展示如何使用反向 Ajax 技术开发事件驱动的 Web 程序。第 1 部分 介绍了反向 Ajax、轮询、流、Comet 和长轮询。第 2 部分 介绍了如何使用 WebSocket,还讨论了使用 Comet 和 WebSocket 的 Web 服务器的限制。第 3 部分 探讨了当您需要支持多个服务器或提供一个用户可以自己的服务器上部署的独立 Web 应用程序时,您实现自己的 Comet 或 WebSocket 通信系统的过程中会遇到的一些困难。第 3 部分还讨论了 Socket.IO。第 4 部分 介绍了 Atmosphere 和 CometD,它们是用于 Java 服务器的最有名的开放源码反向 Ajax 库。
此前,您已经了解了如何通过事件创建组件。在本系列最后一部分中,会应用到事件驱动开发原则,并构建一个事件驱动的 Web 应用程序示例。
您可以下载本文使用的 源代码。
先决条件
在理想的情况下,如果想最大限度地利用本文,您应该了解 JavaScript 和 Java。要运行本文中的示例,则需要使用最新版的 Maven 和 JDK(参阅 参考资料)。
回页首
术语
您可能熟悉事件驱动架构 (EDA)、EventBus 系统、消息传递系统、复杂事件处理 (CEP) 以及通道。这些术语和概念已存在多年。随着技术越来越成熟,您可能会更多地听到它们。本节将简要解释这些概念。
-
事件
- 系统中发生的事情。事件通常拥有一些属性,比如发生日期(时间戳)、源或位置(我们单击的组件)以及一些事件描述数据。根据不同的系统,事件还可以拥有其他属性。 事件处理架构 (EDA)
-
也称为基于事件编程,这是一种架构设计模式,在这种模式中,应用程序包含一些组件,组件通过收发事件进行通信和执行操作。Java Swing 图形用户界面 (GUI) 就是一个 EDA 示例。每个 Swing 组件都可以执行监听事件、做出反应、发送其他事件等操作。EDA 包括几个组成部分:事件生成者、事件使用者、事件以及处理软件。
- 事件生成者:该组件用于发出事件。在本文的示例中,一个表单提交按钮就是一个事件生成者。
- 事件使用者:该组件用于监听特定事件。例如,在表单提交示例中,浏览器将监听表单提交按钮上的单击事件,以便向服务器发送表单数据。
- 事件处理软件:这是事件生成者发布事件和事件使用者注册自己以接收事件的系统内核。根据不同的软件,处理过程可能很简单(只是将生成的事件转发到使用者),也可能很复杂(比如 CEP)。在使用 CEP 时,该软件支持各种处理方式,比如事件汇总、过滤和转换。
Esper 就是这样一个软件。事件处理软件不仅可以是独立运行的应用程序,还可以是应用程序中集成的库。
消息传递系统
- 一种事件驱动应用程序,其中,事件生成者将消息发布到通道,事件使用者订阅通道。事件生成者和使用者之间没有链接,完全独立。在这种事件驱动应用程序中,通常使用术语 消息,而不是 事件。 通道
-
消息传递系统中的事件的一种归类方法。通道表示事件生成者希望事件发送到的目标位置。例如,在一个聊天室应用程序中,通道可能是 /chatapplication/chatrooms/asdrt678。这个通道将标识一个聊天室,事件生成者可以在其中发送消息,图形组件可以订阅该聊天室,以显示新到达的消息。
某些消息传递系统提供两种通道:队列和主题。
- 队列:在将消息发送到队列时,只有一个事件使用者能够接收并处理它。其他使用者看不到它。队列可以是持久性的,以保障消息交付。最好的队列示例可能是一个邮件请求。在用户进行注册的时候,Web 应用程序会将一条消息发布给队列 /myapp/mail/user-registration。可能有几个邮件应用程序订阅该队列。即使没有,消息也不会丢失。
- 主题:在将消息传送到主题时,每个订阅者都会接收到相关消息。主题通常不是持久性的。针对监控软件的主题的一个是 /event/system/cpu/usage,其中一个生成者会定期发送 CPU 的使用情况。另一方面,可能有很多订阅者,也可能完全没有订阅者,这主要取决于订阅者的兴趣。
发布/订阅
- 事件驱动解决方案实现了 “发布/订阅” 模式。事件生成者在处理软件中发布事件,事件使用者通过订阅来接收事件。事件使用者的订阅方式取决于软件。在消息传递应用程序中,事件使用者可以订阅通道(例如,也可以在事件类型上应用过滤规则)。在使用 CEP(比如 Esper)时,可以通过一个 SQL 类请求来实现订阅,从而定义您感兴趣的事件。
回页首
为何使用事件驱动解决方案?
在传统通信方案中,如果系统 A 需要系统 B 中的信息,它会向系统 B 发送一个请求。系统 B 将处理请求,而系统 A 会等待响应。处理完成后,会将响应发送回系统 A。在同步 通信模式下,资源使用效率比较低,这是因为等待响应时会浪费处理时间。
在异步 模式下,系统 A 将订阅它想从系统 B 中获取的信息。然后,系统 A 可以向系统 B 发送一个通知,也可以立即返回信息,与此同时,系统 A 可以处理其他事务。这个步骤是可选的。在事件驱动应用程序中,通常不必请求其他系统发送事件,因为您不知道这些事件是什么。在系统 B 发布响应之后,系统 A 会立即收到该响应。
事件驱动架构的一个好处是它允许实现更好的可伸缩性。可伸缩性 是系统在适应需求、量和强度方面的变化的情况下仍能实现目标的能力。通过消除暂停时间,使得事件驱动架构拥有更好的性能和更高的处理比率。
另一个好处在于应用程序开发和维护。在使用事件驱动解决方案时,每个应用程序组件都可以去除耦合,完全独立。
由于通信延迟减少,事件驱动解决方案能够在更短地时间内进行响应。
回页首
将事件驱动解决方案应用于 Web
Web 框架过去通常依赖传统 “请求-响应” 模式,该模式会导致页面刷新。随着 Ajax、Reverse Ajax 以及 CometD 和 Atmosphere 等强大框架的出现,现在可以将事件驱动架构的概念轻松应用于 Web,获得去耦合、可伸缩性和反应性 (reactivity) 等好处。
在客户端
事件驱动架构可用于客户端的 GUI 开发。您可以拥有充当容器的单个 Web 页,而不是创建传统的 Web 页面。每个元素(页面的每个部分)都可以进行隔离。您可以在 Web 上拥有一个 Java Swing GUI,就像包含小工具的 Google 页面(参见 参考资料 中的链接)。
您还需要一个事件总线。例如,您可以开发一个 JavaScript 事件总线,允许订阅每个页面元素并将它发布到通道。还可以对事件进行同步,以便在收到两个或更多事件后触发操作。事件总线可用于页面中的本地事件,您也可以通过 CometD 或 Socket.IO 使用插件来支持远程事件。
在服务器端
在服务器端上,需要设置一个反向 Ajax 框架来支持事件驱动架构。在本系列前面的文章中介绍过的框架中,只有 CometD 拥有事件驱动方案。要使用其他框架,则需要添加自定义支持,这不太容易。您还可以添加第三方消息传递系统,比如 JMS(例如 Apache ActiveMQ)或一个 CEP(例如 Esper)。Redis 是一个更简单的解决方案,它支持基本 “发布/订阅” 功能。
本系列的主题是事件驱动 Web 和反向 Ajax,因此我们将只关注客户端部分,不会建立一个复杂的消息传递系统。
回页首
事件驱动 Web 示例
本文将创建的示例是一个聊天室 Web 应用程序,它有一个用户面板,其中包含已连接用户的列表。您的用户名为粗体,活动用户(20 秒后仍然活动的用户)显示为绿色,20 秒后不活动的用户显示为橙色。如果一个用户连接或断开连接,列表就会刷新。
出于安全考虑,web.xml 文件中配置了两分钟的会话超时。如果两分钟后仍处于不活动状态,系统会弹出一个窗口,将您重定向到登录页面。
只要您没有进行会话或还没有连接,就会被重定向到登录页面。登录页面要求输入用户名,检查您是否可以登录到聊天室。
登录后,您就可以向聊天室中的所有用户发送消息。您还会看到一个控制台,其中记录了收到的所有事件。
这个 Web 应用程序是事件驱动的。您可以使用上面的信息轻松地定义以下几个事件:
- 用户连接
- 用户断开连接
- 会话过期
- 收到一条聊天消息
- 安全过滤器在您未登录时阻止了一个请求
- 用户变为不活动
- 用户变为活动
- 与 UI 协调相关的其他所有事件
有些事件只是 Web 应用程序的本地事件。它们通过一个本地总线进行标识,如 清单 1 中所示:
清单 1. 总线设置
bus = { local: new EventBus({ name: 'EventBus Local' }), remote: EventBus.cometd({ name: 'EventBus Remote', logLevel: 'warn', url: document.location.href.substring(0, document.location.href.length - document.location.pathname.length) + '/async', onConnect: function() { bus.local.topic('/event/bus/remote/connected').publish(); }, onDisconnect: function() { bus.local.topic('/event/bus/remote/disconnected').publish(); } }) };
另一些事件是远程的,这意味着它们需要通过一个反向 Ajax 系统(比如 CometD)在所有客户端之间发布。图 1 展示了一个示例应用程序。
图 1. 示例应用程序
您可以 下载 示例应用程序。许多类都是适用于安全管理或会话和用户管理的管道类 (plumping class)。本文展示了最重要的代码部分,但建议您下载并运行示例应用程序,以便深入考察其运行方式。
这个 Web 应用程序包含不同的组件:聊天室、用户列表和控制台。每个组件都是独立的,移除一个组件不会影响其他组件。
为了设置事件驱动系统(本地和远程),本示例使用了 Ovea 的 EventBus 系统。它提供一个本地事件总线、一个 CometD 用来获取远程事件的网桥以及一种协调事件(以便在完成几个事件后触发相关操作)的方法。如果愿意,您完全可以使用另一种系统替代它。示例设置使用了 JavaScript,如 清单 1 中所示。
总线设置好之后,应用程序和组件就成为基于事件的应用程序和组件了。在本例中,对 IDLE 检测系统进行了设置,如 清单 2 中所示:
清单 2. IDLE 检测系统
bus.local.topic('/event/dom/loaded').subscribe(function() { $.idleTimer(20000); $(document).bind('idle.idleTimer', function() { bus.local.topic('/event/idle').publish('inactive'); }); $(document).bind('active.idleTimer', function() { bus.local.topic('/event/idle').publish('active'); }); })
使用 清单 2 中的代码,IDLE 系统会在检测到活动时发送事件。这个代码可以用于任何需要 IDLE 系统的应用程序。在本例中,您需要在用户活动事件中转换它。该系统也使用了 JavaScript 实现,如 清单 3 中所示:
清单 3. 用户活动管理
bus.local.topic('/event/idle').subscribe(function(status) { bus.remote.topic('/event/user/status/changed').publish({ status: status == 'active' ? 'online' : 'away' }); }); bus.remote.topic('/event/user/status/changed').subscribe(function(evt) { if(evt.user != me.name) { $('#users li').filter(function() { return evt.user == $(this).data('user').name; }).removeClass('online') .removeClass('away') .addClass(evt.status); } });
第一个订阅从 IDLE 系统接收事件,将用户状态发送给服务器。另一个订阅从服务器接收用户状态事件。这样,只要用户状态变化,用户列表中的用户颜色就会变为绿色或橙色。
用户连接或断开连接时,系统会发送一个事件,如 清单 4 中所示:
清单 4. 用户列表管理
bus.remote.topic('/event/user/connected').subscribe(function(user) { $('#users ul').append(row(user)); }); bus.remote.topic('/event/user/disconnected').subscribe(function(evt) { $('#users li').filter(function() { return evt.user == $(this).data('user').name; }).remove(); });
这个应用程序代码比较简单,消除了耦合并且被隔离。通过重用大量 Ovea 技术,您可以快速创建事件驱动的 Web 应用程序。但是,无需如此,因为另一个系统可以取代它的位置。这个示例只需一天时间即可开发出来,半数代码都是管道代码,包括内容如下:
- Maven,用于构建项目
- 安全特性(登录、退出和会话超时)
- 使用 Jersey 的 RES 服务