简介
现今,用户都期待能够通过 Web 访问快速的动态访问应用。本 系列文章 向您展示如何使用 Reverse Ajax 技术来开发事件驱动的 Web 应用程序。反向 Ajax,第 1 部分:Comet 简介 介绍了 Reverse Ajax、轮询、流、Comet 和长轮询。在您了解了如何通过 HTTP 使用 Comet 之后,就会发现长轮询是可靠地实现 Reverse Ajax 的最佳方式,因为目前所有浏览器都提供了这方面的支持。反向 Ajax,第 2 部分:WebSockets 展示了如何使用 WebSocket 实现 Reverse Ajax。代码示例有助于说明 WebSocket、FlashSocket、服务器端的约束、请求范围内的服务和暂停历时长久的请求。
本文将深入研究如何在不同 Web 容器和 API 的 Web 应用程序(Servlet 3.0 和 Jetty Continuation)中使用 Comet 和 WebSocket。了解如何通过抽象库(如 Socket.IO)透明地使用 Comet 和 WebSocket。Socket.IO 使用功能检测来决定是否将通过 WebSocket、AJAX 长轮询或 Flash 等来创建连接。
您可以 下载本文使用的源代码。
先决条件
在理想情况下,要最大限度地充分利用本文,则应该了解 JavaScript 和 Java。本文创建的示例是使用 Google Guice 构建的。Google Guice 是用 Java 编写的依赖项注入框架。要理解本文内容,则需要熟悉依赖项注入框架的概念,比如 Guice、Spring 或 Pico。
要运行本文中的示例,还需要使用最新版的 Maven 和 JDK (请参阅 参考资料)。
回页首
Comet 和 WebSocket 的服务器解决方案
您在 反向 Ajax,第 1 部分:Comet 简介 中应该已经了解到,Comet(长轮询或流)要求服务器在潜在的长延迟后能够暂停、重启或完成一个请求。反向 Ajax,第 2 部分:WebSockets 描述了服务器需要如何使用非阻塞 I/O 特性来处理一些连接,以及它们如何仅使用线程为请求提供服务(每请求线程模型)。您应该还了解到,WebSocket 的使用取决于服务器,并非所有的服务器都支持 WebSocket。
这一节将向您展示如何在 Jetty、Tomcat 和 Grizzly Web 服务器上使用 Comet 和 WebSocket(如果可以)。本文所提供的 源代码 包含了一个在 Jetty 和 Tomcat 上运行的简单聊天 Web 应用程序样例。本章节还将讨论支持 API 的下列应用服务器:Jboss、Glassfish 和 WebSphere。
Jetty
Jetty 是一个 Web 服务器,它支持 Java Servlet 规范 3.0、WebSocket 和其他的集成规范。Jetty 具有以下特征:
- 强大而又灵活
- 易于嵌入
- 支持虚拟主机、会话集群和一些可以轻易通过 Java 代码进行配置的特性
- 适用于 Google App Engine 的托管服务
核心的 Jetty 项目是由 Eclipse Foundation 管理。
自从版本 6 开始,Jetty 包含了一个异步 API,称为 Jetty Continuation,它可以充许暂停某个请求,稍后再重新开始该请求。表 1 显示了 Jetty 主要版本所支持的规范和 API 的图表。
表 1. Jetty 版本和支持
非阻塞 I/O | X | X | X |
Servlet 2.5 | X | X | X |
Servlet 3.0 | X | X | |
Jetty Continuation (Comet) | X | X | X |
WebSocket | X | X |
要通过 Comet 实现 Reverse Ajax ,可以使用 Jetty 所提供的 Continuation API,如下所示 清单 1:
清单 1. 用于 Comet 的 Jetty Continuation API
// Pausing a request from a servlet's method (get, post, ...): protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { Continuation continuation = ContinuationSupport.getContinuation(req); // optionally set a timeout to avoid suspending requests for too long continuation.setTimeout(0); // suspend the request continuation.suspend(); // then hold a reference for future usage from another thread continuations.offer(continuation); } // Then, from another thread which wants to send an event to the client: while (!continuations.isEmpty()) { Continuation continuation = continuations.poll(); HttpServletResponse response = (HttpServletResponse) continuation.getServletResponse(); // write to the response continuation.complete(); }
完整的 Web 应用程序可在本文所附的 源代码 中找到。Jetty Continuation 存放在 JAR 文件中。您必须将此 JAR 文件放在 Web 应用程序的 WEB-INF/lib 文件夹中,以便能够使用 Jetty 的 Comet 特性。Jetty Continuation 也可以在 Jetty 6、Jetty 7 和 Jetty 8 上使用。
从 Jetty 7 开始,您还可以使用 WebSocket 特性。将 Jetty 的 WebSocket JAR 文件放入 Web 应用程序的 WEB-INF/lib 文件夹中,以便获得访问 Jetty 的 WebSocket API 的访问权,如 清单 2 中所示:
清单 2. Jetty 的 WebSocket API
// Implement the doWebSocketConnect and returns an implementation of // WebSocket: public final class ReverseAjaxServlet extends WebSocketServlet { @Override protected WebSocket doWebSocketConnect(HttpServletRequest request, String protocol) { return [...] } } // Sample implementation of WebSocket: class Endpoint implements WebSocket { Outbound outbound; public void onConnect(Outbound outbound) { this.outbound = outbound; } public void onMessage(byte opcode, String data) { outbound.sendMessage("Echo: " + data); if("close".equals(data)) outbound.disconnect(); } public void onFragment(boolean more, byte opcode, byte[] data, int offset, int length) { } public void onMessage(byte opcode, byte[] data, int offset, int length) { onMessage(opcode, new String(data, offset, length)); } public void onDisconnect() { outbound = null; } }
可下载 源代码 中的 jetty-websocket 文件夹提供了一个聊天示例,它演示了如何使用 Jetty 提供的 WebSocket API。
Tomcat
Tomcat 可能是最广为人知的 Web 服务器。人们使用它已经很多年了,并且它已作为 Web 容器集成到早期版本的 Jboss 应用服务器中。Tomcat 还可以用作 servlet 规范的参考实现。servlet API 2.5 开始停用 Tomcat,人们开始关注基于非阻塞 I/O(如 Jetty)的替代物。表 2 显示了 Tomcat 两个最新版本支持的规范和 API。
表 2. Tomcat 支持
非阻塞 I/O | X | X |
Servlet 2.5 | X | X |
Servlet 3.0 | X | |
Advanced I/O (Comet) | X | X |
WebSocket |
如 表 2 中所示,Tomcat 并不支持 WebSocket;它使用一个与 Jetty Continuation 等效的对等物(叫 Advanced I/O)来支持 Comet。Advanced I/O 是一个包装 NIO 的低级包装器,比优秀的 API 更能促进 Comet 的使用。使用此 API 的应用程序示例相对贪乏,没有几个。清单 3 显示了在聊天 Web 应用程序中用于挂起请求和恢复使用请求的 servlet 示例。您可以在本文里的 源代码 中找到完整的 Web 应用程序。
清单 3. Comet 的 Tomcat API
public final class ChatServlet extends HttpServlet implements CometProcessor { private final BlockingQueueevents = new LinkedBlockingQueue (); public void event(CometEvent evt) throws IOException, ServletException { HttpServletRequest request = evt.getHttpServletRequest(); String user = (String) request.getSession().getAttribute("user"); switch (evt.getEventType()) { case BEGIN: { if ("GET".equals(request.getMethod())) { evt.setTimeout(Integer.MAX_VALUE); events.offer(evt); } else { String message = request.getParameter("message"); if ("/disconnect".equals(message)) { broadcast(user + " disconnected"); request.getSession().removeAttribute("user"); events.remove(evt); } else if (message != null) { broadcast("[" + user + "]" + message); } evt.close(); } } } } void broadcast(String message) throws IOException { Queue q = new LinkedList (); events.drainTo(q); while (!q.isEmpty()) { CometEvent event = q.poll(); HttpServletResponse resp = event.getHttpServletResponse(); resp.setStatus(HttpServletResponse.SC_OK); resp.setContentType("text/html"); resp.getWriter().write(message); event.close(); } } }
在 Tomcat 里,异步 servlet 必须实现一个 CometProcessor
。对于异步 servlet,Tomcat 并不调用标准 HTTP 方法(doGet
和 doPost
等)。相反,会向 event(CometdEvent)
方法发送一个事件。在请求到达时,该示例会查看是否为了挂起该请求而使用了一个 GET
方法;不必调用evt.close()
。如果调用的是一个 POST
方法,则表示用户发送了一个消息,该消息将传播至其他 CometEvent
,并且可以调用evt.close()
来完成请求的传送。在用户端,广播会让所有的长轮询请求来完成消息发送,并且会立即发送另一个长轮询请求来接收下一个事件。
Grizzly 和 Glassfish
Grizzly 并不是一个 Web 容器,它更像是一个帮助开发人员构建可伸缩性应用程序的 NIO 框架。它发展为 Glassfish 项目的一部分,但也可以单独或嵌套使用它。Grizzly 提供了充当 HTTP/HTTPS 服务器的部件,还为 Bayeux Protocol、Servlet、HttpService OSGi 和 Comet 等提供部件。Grizzly 支持 WebSocket,并且可以在 Glassfish 中使用它来支持 Comet 和 WebSocket。
Glassfish(Oracle 的应用服务器)是 J2EE 6 规范的参考实现。Glassfish 是一个完整的套件(像 WebSphere 和 Jboss 一样),它用 Grizzly 来支持 NIO、WebSocket 和 Comet。它的模块架构(基于 OSGI)使得更改部件变得非常灵活。表 3 显示了 Glassfish 对 Comet 和 WebSocket 的支持。
表 3. Glassfish 支持
非阻塞 I/O | X | X |
Servlet 2.5 | X | X |
Servlet 3.0 | X | |
Comet | X | X |
WebSocket | X |
Grizzly 的使用并不繁琐,可以在 Java 代码中嵌套使用或直接使用它。人们普遍将它用作一个框架,用它来支持可以嵌套在大型应用程序(如 Glassfish)中的 Comet 和 WebSocket,这提供了 Web 部署功能和 Servlet 规范 API。
查看 参考资料 中关于 Grizzly 或 Glassfish 中的 WebSocket 和 Comet 示例的链接。因为 Glassfish 使用了 Grizzly,所以两个示例都有效。WebSocket API 与Jetty 中的非常相似,但是 Comet API 更复杂一些。
Jboss
Jboss 是构建于 Tomcat 之上的应用服务器。从版本 5 起,它就开始支持 Comet 和 NIO。Jboss 7 还在开发中,但下面的 表 4 中包含该版本。
表 4. Jboss 支持
非阻塞 I/O | X | X | X |
Servlet 2.5 | X | X | X |
Servlet 3.0 | X | X | |
Comet | X | X | X |
WebSocket |
WebSphere
WebSphere 是一个 IBM 应用服务器。WebSphere V8(参阅 参考资料,阅读相关声明)添加了对 Servlet 3 API(包括 Comet 的标准化异步 API)的支持。
表 5. WebSphere 支持
非阻塞 I/O | X |
Servlet 2.5 | X |
Servlet 3.0 | X |
Comet | X |
WebSocket |
回页首
使用通用 API 会怎样?
每个服务器都自带了用于 Comet 和 WebSocket 的本机 API 。正如您所猜测的,编写一个便携版的 Web 应用程序会非常困难。Servlet 3.0 Specification 包含挂起请求并稍后重新使用请求的其他方法,并充许所有支持 Servlet 3.0 Specification 的 Web 容器支持 Comet 长轮询请求。
Jetty 团队提供了一个名叫 Jetty Continuation 的库,该库独立于 Jetty 容器。Jetty Continuation 库可以智能地检测容器或规范是否可用。如果在 Jetty 服务器上运行,则会使用本机 Jetty API 。如果在支持 Servlet 3.0 规范的容器上运行,则会使用通用的 API。否则会使用不可伸缩的实现。
关于 WebSocket,Java 中没有相关的标准,因此,如果您想要使用 WebSocket,则需要在 Web 应用程序中使用容器供应商 API 。
表 6 概括了各种服务器支持的技术
表 6. 服务器支持的技术
Jetty 6 | Jetty Continuation | N/A |
Jetty 7 | Servlet 3.0 Jetty Continuation |
Native Jetty API |
Jetty 8 | Servlet 3.0 Jetty Continuation |
Native Jetty API |
Tomcat 6 | Advanced I/O | N/A |
Tomcat 7 | Servlet 3.0 Advanced I/O Jetty Continuation |
N/A |
Glassfish 2 | Native Grizzly API | N/A |
Glassfish 3 | Servlet 3.0 本机 Grizzly API Jetty Continuations |
本机 Grizzly API |
Jboss 5 | 本机 Jboss API | N/A |
Jboss 6 | Servlet 3.0 本机 Jboss API Jetty Continuation |
N/A |
Jboss 7 | Servlet 3.0 本机 Jboss API Jetty Continuations |
N/A |
WebSphere 8 | Servlet 3.0 Jetty Continuation |
N/A |
关于 WebSocket, 除了使用容器 API ,没有其他的明确方案。至于 Comet,支持 Servlet 3.0 Specification 的所有容器都支持 Comet。Jetty Continuation 的优势是它在所有的这些容器上都提供了对 Comet 的支持。因此,一些 Reverse Ajax 库(在 下一节 和 本 系列 文章中的下一篇文章中会讨论它)都在对其服务器端 API 使用 Jetty Continuation。
Jetty Continuation API 在本文的 Jetty 示例 中曾描述过。Servlet 3.0 Specification 在本系列 第 1 部分:Comet 简介 的两个 Comet 示例中使用和描述过。
回页首
抽象库
考虑到所有主要的 API(Servlet 3.0 和 Jetty Continuation)、服务器端的所有本机支持以及在客户端实现 Reverse Ajax 的两种主要方法(Comet 和 WebSocket),编写您自己的 JavaScript 和 Java 代码以便将它们连接在一起会非常困难。您还必须考虑超时、连接故障、确认、排序和缓冲等因素。
本文的其余部分将向您介绍 Socket.IO。本 系列 的第 4 部分将探讨 Atmosphere 和 CometD。这三个库均为开源库,它们都支持在许多服务器上使用 Comet 和 WebSocket。
Socket.IO
Socket.IO 是一个 JavaScript 客户端库,可提供一个与 WebSocket 类似的 API,用该 API 连接到远程服务器,以便异步发送和接收消息。通过提供通用 API,Socket.IO 可支持多种传输:WebSocket、Flash Sockets、长轮询、流、forever Iframes 和 JSONP 轮询。Socket.IO 检测浏览器功能并尝试选择可用的最佳传输。Socket.IO 库几乎与所有的浏览器(包括旧版浏览器,如 IE 5.5)以及移动浏览器都兼容。它还提供了心跳、超时、断开连接和错误处理等功能。
Socket.IO 网站(参阅 参考资料)详细描述了库的工作原理以及使用哪种浏览器和 Reverse Ajax 技术。基本上,Socket.IO 使用一种可以使客户端库与服务器端的端点通信的通信协议,以便能够读懂 Socket.IO 协议。Socket.IO 最初是为 Node JS 开发的,它使用一个 JavaScript 引擎来构建快速服务器。许多项目都支持其他语言,其中包括 Java。
清单 4 显示了一个在客户端使用 Socket.IO JavaScript 库的示例。Socket.IO 网站中包含一些文档和示例。
清单 4. Socket.IO 客户端库的使用
var socket = new io.Socket(document.domain, { resource: 'chat' }); socket.on('connect', function() { // Socket.IO is connected }); socket.on('disconnect', function(disconnectReason, errorMessage) { // Socket.IO disconnected }); socket.on('message', function(mtype, data, error) { // The server sent an event }); // Now that the handlers are defined, establish the connection: socket.connect();
要使用 Socket.IO JavaScript 库,则需要一个称为 Socket.IO Java 的相应 Java 部件(参阅 参考资料)。该项目最初由 Apache Wave 团队创建,创建于推出 WebSocket 之前,可利用该项目为 Reverse Ajax 提供支持。Socket.IO Java 是 Ovea(一家专门从事事件驱动 Web 开发的公司)的分支,由 Ovea 维护,后来遭到遗弃。由于有多种传输方式,所以开发后端来支持 Socket.IO 客户端库非常复杂。本系列的第 4 部分将展示如何支持客户端库中的许多传输,这种支持并不是获得更好的可伸缩性和浏览器支持所必需的,因为有长轮询和 WebSocket 就已经足够。在 WebSocket 尚未发布的时候,Socket.IO 确实是一个不错的选择。
Socket.IO Java 使用 Jetty Continuation API 来挂起和重新开始使用请求。它使用本机 Jetty WebSockets API 来支持 WebSocket。您可以使用 Socket.IO Java 确定哪一个服务器将与 Web 应用程序协同工作。
下面的 清单 5 显示了一个如何在服务器上使用 Socket.IO 的示例。您必须定义一个扩展 SocketIOServlet
的 servlet ,并实现返回某种端点表示形式的方法。此 API 与 WebSocket API 非常相似。该 API 的优势在于它可在服务器端使用,独立于客户端所选择的传输方式。Socket.IO 将所有的传输类型转化为与服务器端的 API 相同。
清单 5. 聊天示例 servlet 中使用的 Socket.IO Java 库
public final class ChatServlet extends SocketIOServlet { private final BlockingQueueendpoints = new LinkedBlockingQueue (); @Override protected SocketIOInbound doSocketIOConnect (HttpServletRequest request) { String user = (String) request.getSession().getAttribute("user"); return user == null ? null : new Endpoint(this, user, request); } void broadcast(String data) { for (Endpoint endpoint : endpoints) { endpoint.send(data); } } void add(Endpoint endpoint) { endpoints.offer(endpoint); } void remove(Endpoint endpoint) { endpoints.remove(endpoint); } }
清单 6 显示了如何返回端点。
清单 6. 在聊天示例 Endpoint 中使用的 Socket.IO Java 库
class Endpoint implements SocketIOInbound { [...] private SocketIOOutbound outbound; [...] @Override public void onConnect(SocketIOOutbound outbound) { this.outbound = outbound; servlet.add(this); servlet.broadcast(user + " connected"); } @Override public void onDisconnect(DisconnectReason reason, String errorMessage) { outbound = null; request.getSession().removeAttribute("user"); servlet.remove(this); servlet.broadcast(user + " disconnected"); } @Override public void onMessage(int messageType, String message) { if ("/disconnect".equals(message)) { outbound.close(); } else { servlet.broadcast("[" + user + "] " + message); } } void send(String data) { try { if (outbound != null && outbound.getConnectionState() == ConnectionState.CONNECTED) { outbound.sendMessage(data); } } catch (IOException e) { outbound.close(); } } }