反向Ajax,第4部分:Atmosphere和CometD

 英文原文:Reverse Ajax, Part 4: Atmosphere and CometD

 前言

  这一系列文章展示了如何使用反向Ajax技术开发事件驱动的web应用,第1部分内容介绍了反向Ajax(Reverse Ajax)、polling(轮询)、streaming(流)、Comet和长轮询(long polling);第2部分内容介绍了如何使用WebSocket来实现反向Ajax,并讨论了使用Comet和WebSocket的web服务器的局限性;第3部分内容说明的是,如果需要支持多种服务器或是为用户提供一个部署在他们自己的服务器上的独立web应用的话,实现自己的Comet或是WebSocket通信系统会存在一些难处。即使客户端的JavaScript代码很简单,但你需要用到一些异常处理、重连接和确认功能。在服务器端,全局性API的缺失和多种web服务器API导致了对框架的需求,这带来了一层抽象,第3部分内容还谈到了Socket.IO。

  在本文中,我们了解Atmosphere和CometD,它们是最广为人知的Java服务器的开源反向Ajax库。

  你可以下载本文中使用的源代码。

  前提条件

  理想情况下,要充分体会本文的话,你应该对JavaScrpit和Java有一定的了解。若要运行本文中的例子,你还需要最新版本的Maven和JDK。

  Atmosphere框架

  Atmosphere是一个Java技术框架,其提供了通用的API来使用许多web服务器的Comet和WebSocket,这些web服务器包括了Tomcat、Jetty、GlassFish、Weblogic、Grizzly、JBossWeb、JBoss和Resin,其还支持任何支持Servlet 3.0规范的web服务器。在本系列文章提到的各个框架中,Atmosphere支持的服务器最多。

  Atmosphere可以检测本地化的服务器端API(针对Comet和WebSocket),对于Comet来说,如果可用的话,就切换回Servlet3.0;或者,依然是针对Comet,其会回退到一种“受管”的异步模式中(但没有达到Jetty Continuation的那种可伸缩性)。Atmosphere的存在已经超过了两年的时间,现在依然在处在活跃的发展阶段。其被用在大型的web应用中,比如说JIRA,这是一个最有名的问题追踪器。图1给出了Atmosphere的架构。

  图1. Atmosphere的架构一览

技术分享

  Atmosphere由Atmosphere运行时组成,其为所有不同的web服务器解决方案和标准提供了一个通用的API。在这之上,客户端可以设置一个简单的servlet来通过Google Web Toolkit(GWT)访问该API和反向Ajax功能。或者,你也可以使用Jersey,一个实现了JSR-311(JAX-RS规范)的框架。有了所提供的额外注解,因此Atmosphere可用在RESTful服务中。在配置了所选择的模块后,你就可以通过实现一些类来访问Atomsphere运行时(本文稍后会讨论到)。你还可以选择使用一些提供的插件,这些插件增加了对集群、消息、依赖注入等的支持。如果你正在使用一个web框架(Wecket、Struts、Spring MVC)的话,则可以使用Atmosphere的MeteorServlet来透明地添加反向Ajax支持。这一Servlet暴露出一个Meteor对象,该对象可在你的控制器内部检索到,用来挂起或是恢复请求。

  Atmosphere的强大停留在服务器端:其提供一个了标准的API,该API覆盖了所有与WebSocket或是Comet通信的不同解决方案和方法。Atmosphere并未用到客户端和服务器端之间的协议,比如说Socket.IO和CometD等,这两种库都提供了一个客户端的JavaScript和一个服务器端的servlet,它们的通信用到了一种特定的协议(握手、消息、确认和心跳)。Atmosphere的目标是在服务器端提供一种通用的通信信道。如果你需要用到某种特定协议的话,比如说Bayeux(CometD用到的一个协议),就需要在Atmosphere中开发自己的“处理程序”。CometD插件就是这样做的:其利用了Atmosphere的API来挂起和恢复请求,并委托CometD的类来管理使用了Bayeux协议的CometD通信。

  Atmosphere所带的JQuery客户端库方便了连接的建立,其能够自动检测最好的可用传输方式(WebSocket或是CometD)。Atmosphere的jQuery插件的用法类似于HTML5 WebSocket API,首先你连接到服务器端,注册一个回调来接收信息,然后就可以推一些数据了。

  本文中的源代码包含了一个Atmosphere例子,该类直接用到了一个使用Atmosphere servlet的处理程序。客户端的代码则始终是相同的;与本系列的第1、2和3部分用户的代码一样(使用Comet长轮询的聊天例子)。你有可能使用了Atmosphere的JQuery插件,但这不是必须的,因为Atmosphere并不强制使用任何的通信协议。强烈建议你研究一下Atmosphere项目中的其他例子,特别是用到了JSR-311注解(Jersey)的那些,它们真正地简化了处理程序的编写。

  清单1. AtmosphereHandler接口

 

[java] view plaincopy
 
  1. public interface AtmosphereHandler {  
  2.   void onRequest(AtmosphereResource resource)  
  3.   throws IOException;  
  4.   void onStateChange(AtmosphereResourceEvent event)  
  5.   throws IOException;  
  6.   void destroy();  
  7. }  

 

  onRequest方法接收来自客户端的所有请求并决定是挂起还是恢复它们(或什么也不做),每次挂起或是恢复一个请求、发送一个广播或是有超时发生时,就会发送一个由onStateChange方法接收的事件。

  Comet聊天例子的onRequest方法实现如清单2所示。

  清单2. AtmosphereHandler接口——onRequest

[java] view plaincopy
 
  1. Broadcaster broadcaster = BroadcasterFactory.getDefault().lookup(  
  2.   DefaultBroadcaster.class, ChatHandler.class.getName(), true);  
  3.   broadcaster.setScope(Broadcaster.SCOPE.APPLICATION);  
  4.   resource.setBroadcaster(broadcaster);  
  5.   HttpServletRequest req = resource.getRequest();  
  6.   String user = (String) req.getSession().getAttribute("user");  
  7.   if (user != null) {  
  8.     if ("GET".equals(req.getMethod())) {  
  9.       resource.suspend(-1, false);  
  10.     } else if ("POST".equals(req.getMethod())) {  
  11.       String cmd = req.getParameter("cmd");  
  12.       String message = req.getParameter("message");  
  13.     if ("disconnect".equals(cmd)) {  
  14.       close(resource);  
  15.     } else if (message != null && message.trim().length() > 0) {  
  16.       broadcaster.broadcast("[" + user + "] " + message);  
  17.     }  
  18.   }  
  19. }  

  一种典型的习惯做法是挂起GET请求并使用POST请求来发送消息。在接收到消息时,该消息被广播给所有在广播器内进行了注册的资源。可以注意到,该例子并未往HttpServlet输出流中写入任何东西,广播或是挂起行为只是发送由其他实现方法接收的事件,如清单3所示:

  清单3. AtmosphereHandler接口——onStateChange

[java] view plaincopy
 
  1. Broadcaster broadcaster = BroadcasterFactory.getDefault().lookup(  
  2.   DefaultBroadcaster.class, ChatHandler.class.getName(), true);  
  3.   // Client closed the connection.  
  4.   if (event.isCancelled()) {  
  5.     close(event.getResource());  
  6.     return;  
  7.   }  
  8.   try {  
  9.     String message = (String) event.getMessage();  
  10.     if (message != null) {  
  11.       PrintWriter writer =  
  12.       event.getResource().getResponse().getWriter();  
  13.       writer.write(message);  
  14.       writer.flush();  
  15.     }  
  16.   } finally {  
  17.     if (!event.isResumedOnTimeout()) {  
  18.       event.getResource().resume();  
  19.   }  
  20. }  

  现在你已经具备了用来运作Camet聊天例子的所有所需,概括来说,Atmosphere的一些重要概念是:资源对象描述连接,广播器负责触发资源事件并决定何时挂起或是恢复一个请求。需要注意的是,该例子只适用于Comet,若要能够使用WebSocket和Comet两者的话,应该要使用某种客户端库,且需要一个更复杂的处理程序。

  表1列出了使用Atmosphere框架的利弊。

  表1. Atmosphere的优点和缺点 

1. 优点

如果需要把web应用部署在你不能自己决定的几种web服务器上,那么因为Atmosphere支持许多种web服务器,所以你的应用的反向Ajax功能能够正确工作的机会大大增加。

在没有定义了任何协议的原始的反向Ajax通信之上,因为想要开发或是扩展它,这时你会需要一个通用的API。

2. 缺点

缺乏关于Atmosphere的架构、项目、概念和API的文档,如果需要深入源代码或是分析一些提供的例子的话,这些文档很有帮助。相比于诸如Socket.IO和CometD一类的其他框架的简单API来说,其API的技术性很强,有些很晦涩。即使是在使用Atmosphere的注解时,某些名称和属性也过于专业了。

虽然在服务器端有很好的抽象,但却没有一个很好的客户端库。因为没有协议,故所有的其他功能都留给了开发者来实现。对于一个大的、可伸缩的web应用来说,如果你需要高级的时间检测、确认、回退、跨域等功能的话,特别是在移动设备上运行时,那么目前的库就太过简单了。在这种情况下,CometD更为可靠一些;其利用了一个能够用来激活某些控制流和错误检测的通信协议,所有的这些都在CometD内部提供。如果你需要额外的功能的话,使用Atomsphere CometD插件所带的CometD JavaScript客户端是另一个不错的选择。

  CometD框架

  CometD框架是一个已经存在了好几年的基于HTTP的事件驱动的通信解决方案,其版本2增加了对注解配置和WebSocket的支持。CometD框架提供了一个Java服务器端的部分和一个Java客户端的部分,以及基于JQuery和Dojo的JavaScript客户端库。CometD使用了一个被称作Bayeux的标准的通信协议,允许你激活消息确认、流程控制、同步以及集群等的某些扩展。

  CometD的事件驱动方法非常适合事件驱动的web开发这一新概念,和传统的桌面用户界面一样,所有的组件通信通过一个总线来发送通知和接收事件,因此所有的通信都是异步的。

  CometD框架:

  1. 有详尽的文档说明

  2. 提供了例子和Maven原型来方便项目的启动

  3. 提供了一个精心设计的API来支持扩展开发

  4. 提供了被称为Oort的集群模块,该模块提供了在一个集群中把多个CometD web服务器当作节点来运行的能力,在其之前是一个负载均衡器调节大数据量的HTTP连接。

  5. 支持细粒度的安全策略配置,我们可以指定通过哪一个信道来发送消息。

  6. 很好地整合了Spring和Google Guice(依赖注入框架)

  Bayeux协议

  Byeux通信协议主要是通过HTTP来实现的,其在客户端和服务器端之间以异步的方式提供一个响应式的双向通信。Bayeux协议以消息路由的信道为基础,从客户端向服务器端传递,从服务器端向客户端传递,或是客户端之间传递(但要通过服务器端),Bayeux是一种发布-订阅式的协议。CometD实现了Bayeux协议,从而在Comet和WebSocket传输之上提供了一个抽象层来通过Bayeux路由请求。

  服务器端及其内部

  CometD捆绑了三种传输:JSON、JSONP和WebSocket,它们依赖于Jetty Continuation和Jetty WebSocket API。缺省情况下,CometD可用在Jetty 6、7和8中,以及任何支持Servlet 3.0规范的服务器中。可以通过与扩展一样的方式来增加和开发传输,你应该能够编写传输来支持Grizzly WebSocket API及其他的一些协议,然后在配置CometD服务器的相关步骤中加入它们。图2给出了主要的CometD块的一个概览。

  图2. CometD的架构概览

技术分享

  图2并未给出访问消息信道的安全层。

  本文提供的源代码包括了一个使用了CometD的web应用,这一web应用的描述符包含了清单4中的这一聊天例子的定义。

  清单4. web.xml

[html] view plaincopy
 
  1. <servlet>  
  2. <servlet-name>cometd< /servlet-name>  
  3. <servlet-class>  
  4. org.cometd.java.annotation.AnnotationCometdServlet  
  5. </servlet-class>  
  6. <async-supported>true< /async-supported>  
  7. [...]  
  8. <init-param>  
  9. <param-name>services< /param-name>  
  10. <param-value>ChatService< /param-value>  
  11. </init-param>  
  12. <init-param>  
  13. <param-name>transports< /param-name>  
  14. <param-value>  
  15. com.ovea.cometd.websocket.jetty8.Jetty8WebSocketTransport  
  16. </param-value>  
  17. </init-param>  
  18. </servlet>  

  CometD这一servlet支持控制全局设置的多个选项,比如说设置传输和服务的能力。在该例子中,假设你想要增加Jetty 8的WebSocket支持的话,则服务器端的CometD服务类ChatService会控制每个人都会在其中发言的聊天室,如清单5所示:

  清单5. CometD ChatService

[java] view plaincopy
 
  1. @Service  
  2. public final class ChatService {  
  3.   
  4. @Inject  
  5. BayeuxServer server;  
  6.   
  7. @PostConstruct  
  8. void init() {  
  9. server.addListener(new BayeuxServer.SessionListener() {  
  10. @Override  
  11. public void sessionAdded(ServerSession session) {  
  12. [...]  
  13. }  
  14.   
  15. @Override  
  16. public void sessionRemoved(ServerSession session, boolean timedout) {  
  17. [...]  
  18. }  
  19. });  
  20. }  
  21.   
  22. @Configure("/**")  
  23. void any(ConfigurableServerChannel channel) {  
  24. channel.addAuthorizer(GrantAuthorizer.GRANT_NONE);  
  25. }  
  26.   
  27. @Configure("/chatroom")  
  28. void configure(ConfigurableServerChannel channel) {  
  29. channel.addAuthorizer(new Authorizer() {  
  30. @Override  
  31. public Result authorize(  
  32. [..] // check that the user is in session  
  33. }  
  34. });  
  35. }  
  36.   
  37. @Listener("/chatroom")  
  38. void appendUser(ServerSession remote,  
  39. ServerMessage.Mutable message) {  
  40. [...]  
  41. }  
  42. }  

  清单5说明了CometD的一些主要特征,其中包括:

  1. 依赖注入

  2. 生命周期管理

  3. 全局信道配置

  4. 安全管理

  5. 消息转换(把用户名称添加到所有消息之前)

  6. 会话管理

  在客户端,该例子不会激活任何的扩展——只是原始的CometD代码,如清单6所示:

  清单6. CometD的客户端代码

[javascript] view plaincopy
 
  1. // 先创建cometd对象并配置它  
  2. var cometd = new $.Cometd(‘CometD chat client‘);  
  3. cometd.configure({  
  4. url: document.location + ‘cometd‘,  
  5. logLevel: ‘debug‘  
  6. });  
  7. cometd.websocketEnabled = ‘WebSocket‘ in window;  
  8.   
  9. // 然后注册一些监听器。 说明信道 (有着  
  10. // /meta/格式的那些是具体的预留通道)  
  11. cometd.addListener(‘/meta/disconnect‘, function(message) {  
  12. [...]  
  13. });  
  14.   
  15. cometd.addListener(‘/meta/connect‘, function(message) {  
  16. [...]  
  17. });  
  18.   
  19. // 然后启动一个可以使用的连接:  
  20. cometd.handshake();  
  21.   
  22. // 然后订阅:  
  23. cometd.subscribe(‘/chatroom‘, function(event) {  
  24. [...] // event.data存放消息  
  25. });  
  26.   
  27. // 我们最终以这种方式来发送数据给聊天室:  
  28. cometd.publish(‘/chatroom‘, msg);  

  CometD的客户端API很容易使用和理解,同时又保留了强大的功能和可扩展性。本文只是涵盖了web应用的主要部分,因此可以通过研究例子应用来更好地了解CometD的强大功能。

  表2列出了使用CometD框架的利弊。

  表2. CometD的优点和缺点

1. 优点

从客户端到服务器端,以及从一个独立的Java客户端到服务器端,CometD提供了一个完整的解决方案。框架有详尽的文档说明,有一个很好的API,且非常容易使用。最重要的是,它拥有一种事件驱动的方法。CometD和Bayeux是许多事件驱动应用的构成部分,其他的反向Ajax框架并未提供任何的事件驱动机制,使得最终用户不得不开发自己的定制解决方案。

CometD支持许多必需的功能,比如说重连接、可靠的超时检测、回退、批处理、消息确认,以及更多你不会在其他反向Ajax框架中找得到的功能。CometD可让你实现最可靠的、低延时的通信。

2. 缺点
除了Jetty for Comet(Tomcat)之外,CometD目前并未支持任何的Servlet 2.5容器,其也不支持Glassfish/Grizzly WebSocket。

  结束语

  Atmosphere和CometD都是稳定的、开源的反向Ajax解决方案,我们在Ovea选用的是CometD,因为我们在一个集群环境内部为移动设备开发可伸缩的事件驱动应用,我们完全掌控基础设施(我们使用Jetty)。不过,在没有额外开发的情况下,如果你正在出售web应用且希望你的反向Ajax功能在尽可能多的服务器上都可运作的话,CometD可能不是最好的选择。但现在随着越来越多的Web应用开始支持Servlet 3.0规范,CometD的局限性呈下降趋势。说到传输层,余下的主要差异则取决于对WebSocket的支持。

  代码下载

  reverse_ajaxpt4_source.zip

郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。