Reverse Ajax, Part 2: WebSockets

Introduction

Today, users expect fast, dynamic applications that can be accessed from the Web. This  series  of articles shows how to develop event-driven Web applications using reverse Ajax techniques. Reverse Ajax, Part 1: Introduction to Comet  covers Reverse Ajax, polling, streaming, Comet, and long polling. You should have learned that Comet using HTTP long polling is the best way to reliably implement reverse Ajax, as all browsers now provide support for this.

With this article, you will learn how to implement reverse Ajax using WebSockets. Code samples help illustrate WebSockets, FlashSockets, server-side constraints, request-scoped services, and pausing long-lived requests. You can download the  source code used in this article .

prerequisites

Ideally, you should know JavaScript and Java if you want to get the most out of this article. The samples created in this article were built using Google Guice, a dependency injection framework written in Java. To understand this article, you need to be familiar with the concepts of a dependency injection framework, such as Guice, Spring, or Pico.

To run the examples in this article, you also need the latest version of Maven and JDK (see  Resources ).

 

WebSockets

WebSockets appeared in HTML5, a newer reverse Ajax technology than Comet. WebSockets supports two-way, full-duplex communication channels, and many browsers (Firefox, Google Chrome, and Safari) also support it. Connections go through HTTP requests (also known as WebSockets handshakes) and some special headers. The connection is always active, and you can write and receive data in JavaScript just as you would with a raw TCP socket.

Start the WebSocket URL by entering  ws:// or  wss://(on SSL).

The timeline in Figure 1  shows how to communicate using WebSockets. The HTTP handshake is sent to the server with specific headers. Then, some type of socket can be provided on the JavaScript server or client. You can use this socket to receive data asynchronously through an event handler.

Figure 1. Reverse Ajax via WebSockets

Perform reverse Ajax over WebSockets

 There is a WebSocket example in the download source code for this article  . When you run the example, you get output similar to Listing 1. It shows how events happen on the server side and appear immediately on the client side. When the client sends some data, the server reflects it on the client.

Listing 1. WebSocket example written in JavaScript
[client] WebSocket connection opened
[server] 1 events
[event] ClientID = 0
[server] 1 events
[event] At Fri Jun 17 21:12:01 EDT 2011
[server] 1 events
[event] From 0 : qqq
[server] 1 events
[event] At Fri Jun 17 21:12:05 EDT 2011
[server] 1 events
[event] From 0 : vv

In general, using WebSockets in JavaScript is the same as shown in Listing 2 (if your browser supports it).

Listing 2. JavaScript client-side code
var ws = new WebSocket('ws://127.0.0.1:8080/async');
ws.onopen = function() {
    // called when connection is opened
};
ws.onerror = function(e) {
    // called in case of error, when connection is broken in example
};
ws.onclose = function() {
    // called when connexion is closed
};
ws.onmessage = function(msg) {
    // called when the server sends a message to the client.
    // msg.data contains the message.
};
// Here is how to send some data to the server
ws.send('some data');
// To close the socket:
ws.close();

Any type of data can be sent and received. WebSockets can be thought of as TCP sockets, so it's up to the client and server to decide what type of data to send. The example given here sends a JSON string.

After creating the JavaScript WebSocket object, if you dig into the handshake in the HTTP request in your browser's console (or Firebug), you should see WebSocket-specific headers. Listing 3 shows an example.

Listing 3. Example HTTP request and response headers
Request URL:ws://127.0.0.1:8080/async 
Request Method:GET 
Status Code:101 WebSocket Protocol Handshake 

Request Headers 
Connection:Upgrade 
Host:127.0.0.1:8080 
Origin:http://localhost:8080 
Sec-WebSocket-Key1:1 &1~ 33188Yd]r8dp W75q 
Sec-WebSocket-Key2:1   7;    229 *043M 8 
Upgrade:WebSocket 
(Key3):B4:BB:20:37:45:3F:BC:C7 

Response Headers 
Connection:Upgrade 
Sec-WebSocket-Location:ws://127.0.0.1:8080/async 
Sec-WebSocket-Origin:http://localhost:8080 
Upgrade:WebSocket 
(Challenge Response):AC:23:A5:7E:5D:E5:04:6A:B5:F8:CC:E7:AB:6D:1A:39

所有标头都被 WebSocket 握手用来授权和建立长期连接。WebSocket JavaScript 对象还包含两个有用的属性:

ws.url
返回 WebSocket 服务器的 URL。
ws.readyState
返回当前连接状态的值:
  • CONNECTING = 0
  • OPEN = 1
  • CLOSED = 2

在服务器端,处理 WebSockets 时更加复杂。还没有 Java 规范提供支持 WebSockets 的标准方式。要使用 Web 容器(如 Tomcat 或 Jetty)的 WebSockets 功能,则需要将应用程序代码紧密聚集到使您能够访问 WebSockets 功能的特定于容器的库中。

示例代码 的 websocket 文件夹中的示例使用了 Jetty 的 WebSocket API,因为我们使用的是 Jetty 容器。清单 4 展示了 WebSocket 处理器。(本系列的第 3 部分将使用不同的后端 WebSocket API。)

清单 4. Jetty 容器的 WebSocket 处理器
public final class ReverseAjaxServlet extends WebSocketServlet { 
    @Override 
    protected WebSocket doWebSocketConnect(HttpServletRequest request,
                                           String protocol) { 
        return [...] 
    } 
}

使用 Jetty 时,有许多处理 WebSocket 握手的方法。更简单的方法是为 Jetty 的 WebSocketServlet 创建子类并实现 doWebSocketConnect方法。该方法要求您返回 Jetty 的 WebSocket 接口的一个实例。您需要实现该接口,并返回某种代表 WebSocket 连接的端点。清单 5 提供了一个示例。

清单 5. WebSocket 实现示例
class Endpoint implements WebSocket { 

    Outbound outbound; 

    @Override 
    public void onConnect(Outbound outbound) { 
        this.outbound = outbound;    
    } 

    @Override 
    public void onMessage(byte opcode, String data) { 
        // called when a message is received 
        // you usually use this method 
    } 

    @Override 
    public void onFragment(boolean more, byte opcode, 
                           byte[] data, int offset, int length) { 
        // when a fragment is completed, onMessage is called. 
        // Usually leave this method empty. 
    } 

    @Override 
    public void onMessage(byte opcode, byte[] data, 
                          int offset, int length) { 
        onMessage(opcode, new String(data, offset, length)); 
    } 

    @Override 
    public void onDisconnect() { 
        outbound = null; 
    } 
}

要向客户端发送消息,只需将该消息写入出站即可,如 清单 6 中所示:

清单 6. 向客户端发送一条消息
if (outbound != null && outbound.isOpen()) { outbound.sendMessage('Hello World !'); }

要断开客户端并关闭 WebSocket 连接,可以使用 outbound.disconnect();

WebSockets 是一种非常强大的实现双向通信的方法,且无延迟,Firefox、Google Chrome、Opera 及其他现代浏览器都支持它。根据 jWebSocket 网站上的调查:

  • Chrome 自 4.0.249 开始便包含本机 WebSockets。
  • Safari 5.x 包括本机 WebSockets。
  • Firefox 3.7a6 和 4.0b1+ 包含本机 WebSockets。
  • Opera 从 10.7.9067 开始便包含本机 WebSockets。

如需关于 jWebSocket 的更多信息,请参阅 参考资料

优势

WebSockets 提供强大的、双向、低延迟和易于处理的错误。它没有很多连接,比如:Comet 长轮询,而且也没有 Comet 流的缺点。与 Comet 相比,该 API 易于直接使用,无需使用任何其他层,Comet 需要一个很好的库来处理连接、超时、Ajax 请求、确认以及不同的传输(Ajax 长轮询和 jsonp 轮询)。

缺点

WebSockets 的缺点包括:

  • 它是来自 HTML5 的新规范,并不是所有浏览器都支持它。
  • 无请求作用域。由于 WebSockets 是一个 TCP 套接字,而不是一个 HTTP 请求,因此无法轻松使用请求作用域服务,如 Hibernate 的SessionInViewFilter。Hibernate 是一个持久性框架,提供了一个过滤器来处理 HTTP 请求。请求开始时,将建立一个绑定到请求线程的争用(包含事务和 JDBC 连接)。请求结束后,过滤器会破坏该争用。
 

FlashSockets

对于不支持 WebSockets 的浏览器,一些库能够回退到 FlashSockets(通过 Flash 的套接字)。这些库通常提供相同的官方 WebSocket API,但是它们通过将调用委派给网站上包含的隐藏的 Flash 组件来实现。

优势

FlashSockets 透明地提供 WebSockets 功能,即使在不支持 HTML5 WebSockets 的浏览器上也是如此。

缺点

FlashSockets 具有以下缺点:

  • 它需要安装 Flash 插件(通常所有浏览器都有该插件)。
  • 它要求打开防火墙的 843 端口,以便 Flash 组件能够执行 HTTP 请求来检索包含域授权的策略文件。

    如果无法访问 843 端口,那么库应回退或给出一个错误。所有处理都需要一定的时间(最多 3 秒钟,具体取决于库),这会减慢网站速度。

  • 如果客户端在代理服务器后面,那么到 843 端口的连接可能遭到拒绝。

WebSocketJS 项目提供一个网桥。它要求至少提供 Flash 10,并为 Firefox 3、Internet Explorer 8 和 Internet Explorer 9 提供 WebSockets 支持。

建议

与 Comet 相比,WebSockets 带来很多好处。在日常开发过程中,支持 WebSockets 的客户端的速度变得更快,产生的需求也更少(因此,使用的带宽也更少)。但是,由于并非所有浏览器都支持 WebSockets,因此支持反向 Ajax 库的最佳选择将是能够检测到 WebSockets 支持,如果不支持 WebSockets,则回退到 Comet(长轮询)。

由于需要这两种技术来最大程度地利用所有浏览器并保持兼容性,因此建议您使用在这些技术之上提供抽象层的客户端 JavaScript 库。本系列的第 3 部分和第 4 部分将研究一些库,第 5 部分将展示这些库的应用。在服务器端,情况可能更复杂,如上一节所述。

 

服务器端的反向 Ajax 制约因素

大致了解客户端可用的反向 Ajax 解决方案之后,让我们来看一下服务器上的反向 Ajax 解决方案。到目前为止,示例中主要使用的是客户端 JavaScript 代码。在服务器端,为了接受反向 Ajax 连接,某些技术需要使用特定功能来处理使用期较长的连接(与您熟悉的短 HTTP 请求相比较而言)。为了更好地进行扩展,应该使用新的线程模型,该模型需要使用 Java 中的特定 API 才能够暂停请求。此外,对于 WebSockets,您需要正确管理应用程序中使用的服务的作用域。

线程和非阻塞 I/O

通常情况下,Web 服务器会将每个传入的 HTTP 连接与一个线程或一个进程相关联。这种连接可以是持久的(一直有效),因此多个请求可能使用同一个连接。在本文的示例中,可以将 Apache Web 服务器配置为 mpm_fork 或 mpm_worker 模型来改变这种行为。Java Web 服务器(应用服务器也包括在内)通常为每个传入连接使用一个线程。

生成新的线程会导致内存消耗和资源浪费,因为不能保证生成的线程会被使用。可能会已经建立连接,但没有从客户端或服务器发送数据。无论是否使用该线程,都会消耗内存和 CPU 资源来调度和争用开关。使用线程模型配置服务器时,通常需要配置一个线程池(设置处理传入连接的最大线程数)。如果错误地配置了该值,并且该值过低,那么您将遭遇线程匮乏问题;请求将一直处于等待状态,直到有了可用来处理这些请求的线程。在达到最大并发连接后,响应时间会延长。另一方面,配置较高的线程数可能导致内存不足异常。生成过多的线程会消耗 JVM 的所有堆内存,并导致服务器崩溃。

Java 最近推出了称为非阻塞 I/O 的新的 I/O API。该 API 使用一个选择器,避免每次执行连接到服务器的新 HTTP 连接时都绑定一个线程。有传入数据时,系统会收到一个事件,并分配一个线程来处理请求。因此,这也被称为 “每个请求一个线程” 模型。它允许 WebSphere 和 Jetty 等 Web 服务器进行扩展,并使用固定数量的线程处理越来越多的用户连接。在相同的硬件配置下,在这种模型下运行的 Web 服务器比 “每个连接一个线程” 模型具有更好的扩展性。

在 Philip McCarthy( Comet and Reverse Ajax 的作者)的博客中,他提供了关于两种线程模型的可扩展性的一个有趣基准(请参阅 参考资料 以获得链接)。在 图 2 中,您会发现相同的模型:当使用过多的连接时,线程模型会停止工作。

图 2. 线程模型基准

threading model benchmark

“每个连接一个线程” 模型(图 2 中的线程)通常会提供更快的响应,因为所有线程都已启用、准备就绪并等待使用,但是当连接数量过多时,则会停止服务。在 “每个请求一个线程” 模型下(图 2 的续图),要使用一个线程为到达的请求提供服务,而连接是通过 NIO 选择器进行处理的。响应时间可能会长一些,但是可以将线程回收利用,因此该解决方案在连接数量较大时扩展性更好一些。

为了了解线程的幕后工作方式,可以将 LEGO™ 块想象成为一个选择器。每个传入请求都连接到该 LEGO 块,并通过引脚识别。LEGO 块/选择器将拥有与连接数量相同的 PIN(和密钥)。然后,在等待新事件发生时,只需要使用一个线程在 PIN 上进行迭代。当发生事件时,选择器线程会检索发生事件的密钥,然后使用一个线程为传入请求提供服务。

"Rox Java NIO Tutorial" 提供了使用了用 Java 编译的 NIO 的良好示例(参阅 参考资料)。

 

请求作用域服务

许多框架都提供了服务或过滤器,处理到达 servlet 的 Web 请求。例如,过滤器将执行以下操作:

  • 将 JDBC 连接绑定到请求线程上,整个请求只使用一个连接。
  • 在请求结束时进行变更。

另一个示例是 Google Guice 的 Guice Servlet 扩展(一个依赖项注入库)。与 Spring 一样,Guice 能够在请求的作用域内绑定服务。对每个新请求,一次最多只能创建一个实例(参阅 参考资料 以获得更多相关信息)。

典型用法包括使用来自集群 HTTP 会话的用户 id,缓存从请求中的信息库(如数据库)检索的用户对象。在 Google Guice 中,您可以获得类似于 清单 7 的代码。

清单 7. 请求作用域绑定
@Provides 
@RequestScoped 
Member member(AuthManager authManager, 
              MemberRepository memberRepository) { 
    return memberRepository.findById(authManager.getCurrentUserId());
}

在将一个成员注入某个类时,Guice 会尝试从请求中提取它。如果没有找到它,Guice 会执行信息库调用,并将结果放在请求中。

请求作用域的服务可以与任何反向 Ajax 解决方案配套使用,除了 WebSockets。任何其他解决方案,无论是短期还是长期的,都将依赖于 HTTP 请求,因此每个请求都通过 servlet 调度系统,并执行过滤。完成暂停的(长期)HTTP 请求后,您将在本系列的后续部分看到,还有一个选项可以使请求再次通过过滤器链。

对于 WebSockets,与在 TCP 套接字中一样,数据将直接到达 onMessage 回调。因为没有针对该数据而传入的 HTTP 请求,因此没有决定从哪个请求中获得并存储作用域对象的请求争用。因此,使用需要从 onMessage 回调的作用域对象的服务会失败。

下载源代码 中的 guice-and-websocket 示例展示了如何绕过限制,在 onMessage 回调中仍然使用请求作用域对象。当您运行该示例并单击页面上的每个按钮来测试 Ajax 调用(请求作用域)、WebSocket 调用、带有模拟请求作用域的 WebSocket 调用时,您将获得如 图 3 中所示的输出。

图 3. 使用请求作用域服务的 WebSocket 处理器的输出

The output of the WebSocket handler using the request scoped service

无论使用以下哪个选项,都可能都会遇到这样的问题:

  • Spring。
  • Hibernate。
  • 任何其他需要请求作用域或 “每个请求” 模型的框架,如 OpenSessionInViewFilter
  • 使用 ThreadLocal 工具在过滤器中将变量限制在请求线程中并在以后对其进行访问的系统。

Guice 有一个良好的解决方法,如下所示 清单 8:

清单 8. 从 WebSocket onMessage 回调模拟请求作用域
// The reference to the request is hold when the 
// doWebSocketMethod is called 
HttpServletRequest request = [...] 
Map<Key<?>, Object> bindings = new HashMap<Key<?>, Object>(); 
// I have a service which needs a request to get the session, 
// so I provide the request, but you could provide any other 
// binding that may be needed 
bindings.put(Key.get(HttpServletRequest.class), request); 
ServletScopes.scopeRequest(new Callable<Object>() { 
    @Override 
    public Object call() throws Exception { 
        // call your repository or any service using the scoped objects
        outbound.sendMessage([...]); 
        return null; 
    } 
}, bindings).call();
 

暂停长期请求

使用 Comet 时有另一个障碍。服务器如何能够在不影响性能的前提下暂停长期请求,然后在服务器事件到达时恢复并完成该请求?

显然,您不能只是保留请求和响应,这会导致线程匮乏和内存消耗过高。除了非阻塞 I/O,暂停长轮询请求还需要一个特定的 API。在 Java 中,Servlet 3.0 规范提供了一个这样的 API(参见本系列的 反向 Ajax,第 1 部分:Comet 简介)。清单 9 展示了一个示例。

清单 9. 通过 Servlet 3.0 定义异步 servlet
<?xml version="1.0" encoding="UTF-8"?> 

<web-app version="3.0" xmlns="http://java.sun.com/xml/ns/javaee" 
         xmlns:j2ee="http://java.sun.com/xml/ns/javaee" 
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
         xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml
/ns/j2ee/web-app_3.0.xsd"> 

    <servlet> 
        <servlet-name>events</servlet-name> 
        <servlet-class>ReverseAjaxServlet</servlet-class> 
        <async-supported>true</async-supported> 
    </servlet> 

    <servlet-mapping> 
        <servlet-name>events</servlet-name> 
        <url-pattern>/ajax</url-pattern> 
    </servlet-mapping> 

</web-app>

在定义了异步 servlet 后,您可以使用 Servlet 3.0 API 暂停和恢复请求,如下所示清单 10:

清单 10. 暂停和恢复请求
AsyncContext asyncContext = req.startAsync(); 
// Hold the asyncContext reference somewhere

// Then when needed, in another thread you can resume or complete
HttpServletResponse req = 
    (HttpServletResponse) asyncContext.getResponse(); 
req.getWriter().write("data"); 
req.setContentType([...]); 
asyncContext.complete();

Before Servlet 3.0, each container had (and still has) its own mechanism. Jetty's successor is a well-known example; many reverse Ajax libraries in Java depend on Jetty's successor. This isn't really a hindrance and doesn't require you to run your application in a Jetty container. The API can very intelligently detect which container you are running, and will fall back to the Servlet 3.0 API (if any) when running in another container like Tomcat or Grizzly. The same applies to Comet. But if you want to take full advantage of WebSockets, there is currently no choice but to use container-specific features.

The Servlet 3.0 specification has not yet been published, but many containers have implemented the API because it is the standard way to perform reverse Ajax.

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=326612764&siteId=291194637