反向 Ajax,第 3 部分: Web 服务器和 Socket.IO

简介

现今,用户都期待能够通过 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 版本和支持
支持 Jetty 6 Jetty 7 Jetty 8
非阻塞 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 支持
支持 Tomcat 6 Tomcat 7
非阻塞 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 BlockingQueue<CometEvent> events = 
         new LinkedBlockingQueue<CometEvent>(); 

    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<CometEvent> q = new LinkedList<CometEvent>(); 
        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 支持
支持 Glassfish 2 Glassfish 3
非阻塞 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 支持
支持 Jboss 5 Jboss 6 Jboss 7
非阻塞 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 支持
支持 WebSphere 8
非阻塞 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. 服务器支持的技术
容器 Comet WebSocket
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 BlockingQueue<Endpoint> endpoints = 
        new LinkedBlockingQueue<Endpoint>(); 
 
    @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(); 
        } 
    } 
 
}

猜你喜欢

转载自aoyouzi.iteye.com/blog/2311712
今日推荐