反向ajax实现

在过去的几年中,web开发已经发生了很大的变化。现如今,我们期望的是能够通过web快速、动态地访问应用。在这一新的文章系列中,我们学习如何使用反 向Ajax(Reverse Ajax)技术来开发事件驱动的web应用,以此来实现更好的用户体验。客户端的例子使用的是JQuery JavaScript库,在这首篇文章中,我们探索不同的反向Ajax技术,使用可下载的例子来学习使用了流(streaming)方法和长轮询(long polling)方法的Comet。

  前言

  web开发在过去的几年中有了很大的进展,我们已经远超了把静态网页链接在一起的做法,这种做法会引起浏览器的刷新,并且要等待页面的加载。现在需要 的是能够通过web来访问的完全动态的应用,这些应用通常需要尽可能的快,提供近乎实时的组件。在这一新的由五部分组成的文章系列中,我们学习如何使用反 向Ajax(Reverse Ajax)技术来开发事件驱动的web应用。

  在这第一篇文章中,我们要了解反向Ajax、轮询(polling)、流(streaming)、Comet和长轮询(long polling),学习如何实现不同的反向Ajax通信技术,并探讨每种方法的优点和缺点。你可以下载本文中例子的相应源代码。

  Ajax、反向Ajax和WebSocket

  异步的JavaScript和XML(Asynchronous JavaScript and XML,Ajax),一种可通过JavaScript来访问的浏览器功能特性,其允许脚本向幕后的网站发送一个HTTP请求而又无需重新加载页面。Ajax的出现已经超过了十年,尽管其名字中包含了XML,但你几乎可以在Ajax请求中传送任何的东西,最常用的数据是JSON,其与JavaScript语法很接近,且消耗更少带宽。清单1给出了这样的一个例子,Ajax请求通过某个地方的邮政编码来检索该地的名称。

  清单1. Ajax请求举例

复制代码
var url ='http://www.geonames.org/postalCodeLookupJSON?postalcode='
  + $('#postalCode').val() +'&country='
  + $('#country').val() +'&callback=?';
  $.getJSON(url, function(data) {
  $('#placeName').val(data.postalcodes[0].placeName);
});
复制代码

  在本文可下载的源代码中,你可在listing1.html中看到这一例子的作用。

  反向Ajax(Reverse Ajax)本质上则是这样的一种概念:能够从服务器端向客户端发送数据。在一个标准的HTTP Ajax请求中,数据是发送给服务器端的,反向Ajax可以某些特定的方式来模拟发出一个Ajax请求,这些方式本文都会论及,这样的话,服务器就可以尽可能快地向客户端发送事件(低延迟通信)。

  WebSocket技术来自HTML5,是一种最近才出现的技术,许多浏览器已经支持它(Firefox、Google Chrome、Safari等等)。WebSocket启 用双向的、全双工的通信信道,其通过某种被称为WebSocket握手的HTTP请求来打开连接,并用到了一些特殊的报头。连接保持在活动状态,你可以用 JavaScript来写和接收数据,就像是正在用一个原始的TCP套接口一样。WebSocket会在这一文章系列的第二部分中谈及。

  反向Ajax技术

  反向Ajax的目的是允许服务器端向客户端推送信息。Ajax请求在缺省情况下是无状态的,且只能从客户端向服务器端发出请求。你可以通过使用技术模拟服务器端和客户端之间的响应式通信来绕过这一限制。

  HTTP轮询和JSONP轮询

  轮询(polling)涉及了从客户端向服务器端发出请求以获取一些数据,这显然就是一个纯粹的Ajax HTTP请求。为了尽快地获得服务器端事件,轮询的间隔(两次请求相隔的时间)必须尽可能地小。但有这样的一个缺点存在:如果间隔减小的话,客户端浏览器就会发出更多的请求,这些请求中的许多都不会返回任何有用的数据,而这将会白白地浪费掉带宽和处理资源。

  图1中的时间线说明了客户端发出了某些轮询请求,但没有信息返回这种情况,客户端必须要等到下一个轮询来获取两个服务器端接收到的事件。

  图1. 使用HTTP轮询的反向Ajax

     

 

 

  JSONP轮询基本上与HTTP轮询一样,不同之处则是JSONP可以发出跨域请求(不是在你的域内的请求)。清单1使用JSONP来通过邮政编码获取地名,JSONP请求通常可通过它的回调参数和返回内容识别出来,这些内容是可执行的JavaScript代码。

  要在JavaScript中实现轮询的话,你可以使用setInterval来定期地发出Ajax请求,如清单2所示:

  清单2. JavaScript轮询

setInterval(function() {
  $.getJSON('events', function(events) {
    console.log(events);
  });
}, 2000);

  文章源代码中的轮询演示给出了轮询方法所消耗的带宽,间隔很小,但可以看到有些请求并未返回事件,清单3给出了这一轮询示例的输出。

  清单3. 轮询演示例子的输出

复制代码
[client] checking for events...
[client] no event
[client] checking for events...
[client]2 events
[event] At Sun Jun 0515:17:14 EDT 2011
[event] At Sun Jun 0515:17:14 EDT 2011
[client] checking for events...
[client]1 events
[event] At Sun Jun 0515:17:16 EDT 2011
复制代码

  用JavaScript实现的轮询的优点和缺点:

  1. 优点:很容易实现,不需要任何服务器端的特定功能,且在所有的浏览器上都能工作。

  2. 缺点:这种方法很少被用到,因为它是完全不具伸缩性的。试想一下,在100个客户端每个都发出2秒钟的轮询请求的情况下,所损失的带宽和资源数量,在这种情况下30%的请求没有返回数据。

  Piggyback

  捎带轮询(piggyback polling)是一种比轮询更加聪明的做法,因为它会删除掉所有非必需的请求(没有返回数据的那些)。不存在时间间隔,客户端在需要的时候向服务器端发送请求。不同之处在于响应的那部分上,响应被分成两个部分:对请求数据的响应和对服务器事件的响应,如果任何一部分有发生的话。图2给出了一个例子。

  图2. 使用了piggyback轮询的反向Ajax

  在实现piggyback技术时,通常针对服务器端的所有Ajax请求可能会返回一个混合的响应,文章的下载中有一个实现示例,如下面的清单4所示。

  清单4. piggyback代码示例

复制代码
$('#submit').click(function() {
  $.post('ajax', function(data) {
    var valid = data.formValid;
    // 处理验证结果
    // 然后处理响应的其他部分(事件)
    processEvents(data.events);
  });
});
复制代码

  清单5给出了一些piggyback输出。

  清单5. piggyback输出示例

复制代码
[client] checking for events...
[server] form valid ? true
[client]4 events
[event] At Sun Jun 0516:08:32 EDT 2011
[event] At Sun Jun 0516:08:34 EDT 2011
[event] At Sun Jun 0516:08:34 EDT 2011
[event] At Sun Jun 0516:08:37 EDT 2011
复制代码

  你可以看到表单验证的结果和附加到响应上的事件,同样,这种方法也有着一些优点和缺点:

  1. 优点:没有不返回数据的请求,因为客户端对何时发送请求做了控制,对资源的消耗较少。该方法也是可用在所有的浏览器上,不需要服务器端的特殊功能。

  2. 缺点:当累积在服务器端的事件需要传送给客户端时,你却一点都不知道,因为这需要一个客户端行为来请求它们。

  Comet

  使用了轮询或是捎带的反向Ajax非常受限:其不具伸缩性,不提供低延迟通信(只要事件一到达服务器端,它们就以尽可能快的速度到达浏览器端)。 Comet是一个web应用模型,在该模型中,请求被发送到服务器端并保持一个很长的存活期,直到超时或是有服务器端事件发生。在该请求完成后,另一个长 生存期的Ajax请求就被送去等待另一个服务器端事件。使用Comet的话,web服务器就可以在无需显式请求的情况下向客户端发送数据。

  Comet的一大优点是,每个客户端始终都有一个向服务器端打开的通信链路。服务器端可以通过在事件到来时立即提交(完成)响应来把事件推给客户端, 或者它甚至可以累积再连续发送。因为请求长时间保持打开的状态,故服务器端需要特别的功能来处理所有的这些长生存期请求。图3给出了一个例子。(这一文章 系列的第2部分会更加详细地解释服务器端的约束条件)。

  图3. 使用Comet的反向Ajax

  Comet的实现可以分成两类:使用流(streaming)的那些和使用长轮询(long polling)的那些。

  使用HTTP流的Comet

  在流(streaming)模式中,有一个持久连接会被打开。只会存在一个长生存期请求(图3中的#1),因为每个到达服务器端的事件都会通过这同一 连接来发送。因此,客户端需要有一种方法来把通过这同一连接发送过来的不同响应分隔开来。从技术上来讲,两种常见的流技术包括Forever Iframe(隐藏的IFrame),或是被用来在JavaScript中创建Ajax请求的XMLHttpRequest对象的多部分(multi- part)特性。

  Forever Iframe

  Forever Iframe(永存的Iframe)技术涉及了一个置于页面中的隐藏Iframe标签,该标签的src属性指向返回服务器端事件的servlet路径。每 次在事件到达时,servlet写入并刷新一个新的script标签,该标签内部带有JavaScript代码,iframe的内容被附加上这一 script标签,标签中的内容就会得到执行。

  1. 优点:实现简单,在所有支持iframe的浏览器上都可用。

  2. 缺点: 没有方法可用来实现可靠的错误处理或是跟踪连接的状态,因为所有的连接和数据都是由浏览器通过HTML标签来处理的,因此你没有办法知道连接何时在哪一端已被断开了。

  Multi-part XMLHttpRequest

  第二种技术,更可靠一些,是XMLHttpRequest对象上使用某些浏览器(比如说Firefox)支持的multi-part标志。Ajax请 求被发送给服务器端并保持打开状态,每次有事件到来时,一个多部分的响应就会通过这同一连接来写入,清单6给出了一个例子。

  清单6. 设置Multi-part XMLHttpRequest的JavaScript代码示例

复制代码
var xhr = $.ajaxSettings.xhr();
xhr.multipart =true;
xhr.open('GET', 'ajax', true);
xhr.onreadystatechange = function() {
  if (xhr.readyState == 4) {
    processEvents($.parseJSON(xhr.responseText));
  }
};
xhr.send(null);
复制代码

  在服务器端,事情要稍加复杂一些。首先你必须要设置多部分请求,然后挂起连接。清单7展示了如何挂起一个HTTP流请求。(这一系列的第3部分会更加详细地谈及这些API。)

  清单7. 使用Servlet 3 API来在servlet中挂起一个HTTP流请求

复制代码
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
  // 开始请求的挂起
  AsyncContext asyncContext = req.startAsync();
  asyncContext.setTimeout(0);

  // 给客户端发回多部分的分隔符
  resp.setContentType("multipart/x-mixed-replace;boundary=\""
  + boundary +"\"");
  resp.setHeader("Connection", "keep-alive");
  resp.getOutputStream().print("--"+ boundary);
  resp.flushBuffer();

  // 把异步上下文放在列表中以被将来只用
  asyncContexts.offer(asyncContext);
}
复制代码

  现在,每次有事件发生时你都可以遍历所有的挂起连接并向它们写入数据,如清单8所示:

  清单8. 使用Servlet 3 API来向挂起的多部分请求发送事件

复制代码
for (AsyncContext asyncContext : asyncContexts) {
  HttpServletResponse peer = (HttpServletResponse)
  asyncContext.getResponse();
  peer.getOutputStream().println("Content-Type: application/json");
  peer.getOutputStream().println();
  peer.getOutputStream().println(new JSONArray()
  .put("At "+new Date()).toString());
  peer.getOutputStream().println("--"+ boundary);
  peer.flushBuffer();
}
复制代码

  本文可下载文件的Comet-straming文件夹中的部分说明了HTTP流,在运行例子并打开主页时,你会看到只要事件一到达服务器端,虽然不同 步但它们几乎立刻会出现在页面上。而且,如果打开Firebug控制台的话,你就能看到只有一个Ajax请求是打开的。如果再往下看一些,你会看到 JSON响应被附在Response选项卡中,如图4所示:

  图4. HTTP流请求的FireBug视图

  照例,做法存在着一些优点和缺点:

  1. 优点:只打开了一个持久连接,这就是节省了大部分带宽使用率的Comet技术。

  2. 缺点:并非所有的浏览器都支持multi-part标志。某些被广泛使用的库,比如说用Java实现的CometD,被报告在缓冲方面有问题。例如,一些 数据块(多个部分)可能被缓冲,然后只有在连接完成或是缓冲区已满时才被发送,而这有可能会带来比预期要高的延迟。

  使用HTTP长轮询的Comet

  长轮询(long polling)模式涉及了打开连接的技术。连接由服务器端保持着打开的状态,只要一有事件发生,响应就会被提交,然后连接关闭。接下来。一个新的长轮询连接就会被正在等待新事件到达的客户端重新打开。

  你可以使用script标签或是单纯的XMLHttpRequest对象来实现HTTP长轮询。

  script标签

  正如iframe一样,其目标是把script标签附加到页面上以让脚本执行。服务器端则会:挂起连接直到有事件发生,接着把脚本内容发送回浏览器,然后重新打开另一个script标签来获取下一个事件。

  1. 优点:因为是基于HTML标签的,所有这一技术非常容易实现,且可跨域工作(缺省情况下,XMLHttpRequest不允许向其他域或是子域发送请求)。

  2. 缺点:类似于iframe技术,错误处理缺失,你不能获得连接的状态或是有干涉连接的能力。

  XMLHttpRequest长轮询

  第二种,也是一种推荐的实现Comet的做法是打开一个到服务器端的Ajax请求然后等待响应。服务器端需要一些特定的功能来允许请求被挂起,只要一 有事件发生,服务器端就会在挂起的请求中送回响应并关闭该请求,完全就像是你关闭了servlet响应的输出流。然后客户端就会使用这一响应并打开一个新 的到服务器端的长生存期的Ajax请求,如清单9所示:

  清单9. 设置长轮询请求的JavaScript代码示例

复制代码
function long_polling() {
  $.getJSON('ajax', function(events) {
    processEvents(events);
    long_polling();
  });
}
long_polling();
复制代码

  在后端,代码也是使用Servlet 3 API来挂起请求,正如HTTP流的做法一样,但你不需要所有的多部分处理代码,清单10给出了一个例子。

  清单10. 挂起一个长轮询Ajax请求

复制代码
protected void doGet(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
  AsyncContext asyncContext = req.startAsync();
  asyncContext.setTimeout(0);
  asyncContexts.offer(asyncContext);
}
复制代码

  在接收到事件时,只是取出所有的挂起请求并完成它们,如清单11所示:

  清单11. 在有事件发生时完成长轮询Ajax请求

复制代码
while (!asyncContexts.isEmpty()) {
  AsyncContext asyncContext = asyncContexts.poll();
  HttpServletResponse peer = (HttpServletResponse)
  asyncContext.getResponse();
  peer.getWriter().write(
    new JSONArray().put("At " + new Date()).toString());
  peer.setStatus(HttpServletResponse.SC_OK);
  peer.setContentType("application/json");
  asyncContext.complete();
}
复制代码

  在附带的下载源文件中,comet-long-polling文件夹包含了一个长轮询示例web应用,你可以使用 mvn jetty:run 命令来运行它。

  1. 优点:客户端很容易实现良好的错误处理系统和超时管理。这一可靠的技术还允许在与服务器端的连接之间有一个往返,即使连接是非持久的(当你的应用有许多的 客户端时,这是一件好事)。它可用在所有的浏览器上;你只需要确保所用的XMLHttpRequest对象发送到的简单的Ajax请求就可以了。

  2. 缺点:相比于其他技术来说,不存在什么重要的缺点,像所有我们已经讨论过的技术一样,该方法依然依赖于无状态的HTTP连接,其要求服务器端有特殊的功能来临时挂起连接。

  建议

  因为所有现代的浏览器都支持跨域资源共享(Cross-Origin Resource Share,CORS)规范,该规范允许XHR执行跨域请求,因此基于脚本的和基于iframe的技术已成为了一种过时的需要。

  把Comet做为反向Ajax的实现和使用的最好方式是通过XMLHttpRequest对象,该做法提供了一个真正的连接句柄和错误处理。考虑到不 是所有的浏览器都支持multi-part标志,且多部分流可能会遇到缓冲问题,因此建议你选择经由HTTP长轮询使用XMLHttpRequest对象 (在服务器端挂起的一个简单的Ajax请求)的Comet模式,所有支持Ajax的浏览器也都支持该种做法。

  结论

  本文提供的是反向Ajax技术的一个入门级介绍,文章探索了实现反向Ajax通信的不同方法,并说明了每种实现的优势和弊端。你的具体情况和应用需求 将会影响到你对最合适方法的选择。不过一般来说,如果你想要在低延迟通信、超时和错误检测、简易性,以及所有浏览器和平台的良好支持这几方面有一个最好的 折中的话,那就选择使用了Ajax长轮询请求的Comet。

 

这一文章系列探讨了如何使用反向Ajax(Reverse Ajax)技术来开发事件驱动的web应用,第1部分内容介绍了实现反向Ajax通信的几种不同方式:轮询(polling)、捎带(piggyback)以及使用了长轮询(long-polling)和流(streaming)的Comet。在本文中,我们学习一种新的实现反向Ajax的技术:使用WebSocket,一个新的HTML5 API。WebSocket可由浏览器厂商来做本地化实现,或是通过把调用委托给隐藏的被称为FlashSocket的Flash组件这种桥接手段来实现。本文还讨论了反向Ajax技术带来的一些服务器端约束。

  前言

  时至今日,用户期待的是可通过web访问快速、动态的应用。这一文章系列展示了如何使用反向Ajax(Reverse Ajax)技术来开发事件驱动的web应用。该系列的第1部分介绍了反向Ajax、轮询(polling)、流(streaming)、Comet和长轮询(long polling)。你已经了解了Comet是如何使用HTTP长轮询的,这是可靠地实现反向Ajax的最好方式,因为现有的所有浏览器都提供支持。

  在本文中,我们将学习如何使用WebSocket来实现反向Ajax。代码例子被用来帮助说明WebSocket、FlashSocket、服务器端约束、请求作用域(request-scoped)服务以及暂停长生存期请求等,你可以下载本文中用到的这些源代码。

  前提条件

  理想情况下,要充分体会本文的话,你应该对JavaScrpit和Java有一定的了解。本文中创建的例子是使用Google Guice来构建的,这是一个使用Java编写的依赖注入框架。若要读懂文中所谈内容,你应该要熟悉诸如Guice、Spring或是Pico一类的依赖注入框架的概念。

  若要运行本文中的例子,你还需要最新版本的Maven和JDK(参见参考资料)。

  WebSocket

  在HTML5中出现的WebSocket是一种比Comet还要新的反向Ajax技术,WebSocket启用了双向的全双工通信信道,许多浏览器(Firefox、Google Chrome和Safari)都已对此做了支持。连接是通过一个被称为WebSocket握手的HTTP请求打开的,其用到了一些特殊的报头。连接会保持在活动状态,你可以使用JavaScript来写入和接收数据,就像是在使用一个原始的TCP套接口一样。

  WebSocket URL的起始输入是ws://或是wss://(在SSL上)。

  图1中的时间线说明了使用WebSocket的通信。一个带有特定报头的HTTP握手被发送到了服务器端,接着在服务器端或是客户端就可以通过JavaScript来使用某种套接口(socket)了,这一套接口可被用来通过事件句柄异步地接收数据。

  图1. 使用WebSocket的反向Ajax

  本文可下载的源代码中有一个WebSocket例子,在运行该例子时,你应该会看到类似清单1的输出。其说明了客户端的事件是如何发生的,以及如何会立即在客户端显示出来。当客户端发送一些数据时,服务器端回应客户端的发送行为。

  清单1. JavaScript中的WebSocket例子

[client] WebSocket connection opened
[server] 1 events
[event] ClientID =0
[server] 1 events
[event] At Fri Jun 1721:12:01 EDT 2011
[server] 1 events
[event] From 0 : qqq
[server] 1 events
[event] At Fri Jun 1721:12:05 EDT 2011
[server] 1 events
[event] From 0 : vv

  通常情况下,在JavaScript中你会如清单2所说明的那样来使用WebSocket,如果你的浏览器支持它的话。

  清单2. JavaScript客户端例子

var ws = new WebSocket('ws://127.0.0.1:8080/async');
ws.onopen = function() {
    // 连接被打开时调用
};
ws.onerror = function(e) {
    // 在出现错误时调用,例如在连接断掉时
};
ws.onclose = function() {
    // 在连接被关闭时调用
};
ws.onmessage = function(msg) {
    // 在服务器端向客户端发送消息时调用
    // msg.data包含了消息
};
// 这里是如何给服务器端发送一些数据
ws.send('some data');
// 关闭套接口
ws.close();

  发送和接收的数据可以是任意类型的,WebSocket可被看成是TCP套接口,因此这取决于客户端和服务器端知道要来回发送的数据是哪种类型的。这里的例子发送的是JSON串。

  在JavaScript WebSocket对象被创建后,如果在浏览器的控制台(或是Firebug)中仔细看一下HTTP请求的话,你应该会看到WebSocket特有的报头。清单3给出了一个例子。

  清单3. HTTP请求和相应报头示例

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:17; 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:返回当前连接状态的值
  1. CONNECTING = 0
  2. OPEN = 1
  3. CLOSED = 2

  服务器端对WebSocket的处理要稍加复杂一些,现在还没有某个Java规范以一种标准的方式来支持WebSocket。要使用web容器(例如Tomcat或是Jetty)的WebSocket功能的话,你得把应用代码和容器特定的库紧密耦合在一起才能访问WebSocket的功能。

  示例代码的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连接的某种端点(endpoint)。清单5提供了一个例子。

  清单5. WebSocket实现示例

class Endpoint implements WebSocket {
Outbound outbound;
@Override
publicvoid onConnect(Outbound outbound) {
  this.outbound = outbound;
}
@Override
publicvoid onMessage(byte opcode, String data) {
  // 在接收到消息时调用
  // 你通常用到的就是这一方法
}
@Override
publicvoid onFragment(boolean more, byte opcode,byte[] data, int offset, int length) {
  // 在完成一段内容时,onMessage被调用
  // 通常不在这一方法中写入东西
}
@Override
publicvoid onMessage(byte opcode, byte[] data,int offset, int length) {
  onMessage(opcode, new String(data, offset, length));
}
@Override
publicvoid onDisconnect() {
  outbound =null;
}
}

  若要向客户端发送消息的话,你要向outbound中写入消息,如果清单6所示:

  清单6. 发送消息给客户端

if (outbound != null && outbound.isOpen()) {
  outbound.sendMessage('Hello World !');
}

  要断开并关闭到客户端的WebSocket连接的话,使用outbound.disconnect()。

  WebSocket是一种实现无延迟双向通信的非常强大的方法,Firefox、Google Chrome、Opera和其他的现代浏览器都支持这种做法。根据jWebSocket网站的说法:

  1. Chrome从4.0.249版本开始包含本地化的WebSocket。
  2. Safari 5.x包含了本地化的WebSocket。
  3. Firefox 3.7a6和4.0b1+包含了本地化的WebSocket。
  4. Opera从10.7.9.67开始包含了本地化的WebSocket。

  欲了解更多关于jWebSocket方面的内容,请查阅参考资料。

  优点

  WebSocket功能强大、双向、低延迟,且易于处理错误,其不会像Comet长轮询那样有许多的连接,也没有Comet流所具有的一些缺点。它的API也很容易使用,无需另外的层就可以直接使用,而Comet则需要一个很好的库来处理重连接、超时、Ajax请求、确认以及选择不同的传输(Ajax长轮询和jsonp轮询)。

  缺点

  WebSocket的缺点有这些:

  1. 是一个来自HTML5的新规范,还没有被所有的浏览器支持。

  2. 没有请求作用域(request scope),因为WebSocket是一个TCP套接口而不是一个HTTP请求,有作用域的请求服务,比如说Hibernate的SessionInViewFilter,就不太容易使用。Hibernate是一个持久性框架,其在HTTP请求的外围提供了一个过滤器。在请求开始时,其在请求线程中设定了一个上下文(包括事务和JDBC连接)边界;在请求结束时,过滤器销毁这一上下文。

  FlashSocket

  对于不支持WebSocket的浏览器来说,有些库能够回退到FlashSocket(经由Flash的套接口)上。这些库通常会提供同样的官方WebSocket API,但他们是通过把调用委托给一个包含在网站中的隐藏的Flash组件来实现的。

  优点

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

  缺点

  FlashSocket有着下面的这些缺点:

  1. 其需要安装Flash插件(通常情况下,所有浏览器都会有该插件)。

  2. 其要求防火墙的843端口是打开的,这样Flash组件才能发出HTTP请求来检索包含了域授权的策略文件。如果843端口是不可到达的话,则库应该有回退动作或是给出一个错误,所有的这些处理都需要一些时间(最多3秒,这取决于库),而这会降低网站的速度。

  3. 如果客户端处在某个代理服务器的后面的话,到端口843的连接可能会被拒绝。

  WebSocketJS项目提供了一种桥接方式,其要求一个至少是10版本的Flash来为Firefox 3、Inernet Explorer 8和Internet Explorer 9提供WebSocket支持。

  建议

  相比于Comet,WebSocket带来了更多的好处。在日常开发中,客户端支持的WebSocket速度更快,且产生较少的请求(从而消耗更少的带宽)。不过,由于并非所有的浏览器都支持WebSocket,因此,对于Reverse Ajax库来说,最好的选择就是能够检测对WebSocket的支持,并且如果不支持WebSocket的话,还能够回退到Comet(长轮询)上。

  由于这两种技术需要从所有浏览器中获得最好的做法并保持兼容性,因此我的建议是使用一个客户端的JavaScript库,该库在这些技术之上提供一个抽象层。本系列的第3和第4部分内容会探讨一些库,第5部分则是说明它们的应用。在服务器端,正如下一节内容讨论的那样,事情则会稍加复杂一些。

  服务器端的反向Ajax约束

  现在你对客户端可用的反向Ajax解决方案已经有了一个概观,让我们再来看看服务器端的反向Ajax解决方案。到目前为止,例子使用的都还主要是客户端的JavaScript代码。在服务器端,要接受反向Ajax连接的话,相比你所熟悉的短HTTP请求,某些技术需要特定的功能来处理长生存期的连接。为了得到更好的伸缩性,应该要使用一种新的线程模型,该模型需要Java中的某个特定API来暂停请求。还有,就WebSocket来说,你必须要正确地管理应用中用到的服务的作用域。

  线程和非阻塞I/O

  通常情况下,web服务器会把一个线程或是一个进程与每个传入的HTTP连接关联起来。这一连接可以是持久的(保持活动),这样多个请求就可以通过这同一个连接进行了。在本文的例子中,Apache web服务器可以配置成mpm_fork或是mpm_worker模式来改变这一行为。Java web服务器(应用服务器也包括在内——这是同一回事)通常会为每个传入的连接使用单独的一个线程。

  产生一个新的线程会带来内存的消耗和资源的浪费,因为其并不保证产生的线程会被用到。连接可能会建立起来,但是没有来自客户端或是服务器端的数据在发送。不管这一线程是否被用到,其都会消耗用于调度和上下文切换的内存和CPU资源。而且,在使用线程模式来配置服务器时,你通常需要配置一个线程池(设定处理传入连接的线程的最大数目)。如果该值配置不当,值太小的话,你最终就会遭遇线程饥饿问题;请求就会一直处于等待状态直到有线程可用来处理它们,在达到最大并发连接时,响应时间就会下降。另一方面,配置一个高值则可会导致内存不足的异常,产生过多线程会消耗尽JVM的所有可用的堆,导致服务器崩溃。

  Java最近引入一个新的I/O API,其被称为非阻塞式的I/O。这一API使用一个选择器来避免每次有新的HTTP连接在服务器端建立时都要绑定一个线程的做法,当有数据到来时,就会有一个事件被接收,接着某个线程就被分配来处理该请求。因此,这种做法被称为每个请求一个线程(thread-per-request)模式。其允许web服务器,比如说WebSphere和Jetty等,使用固定数量的线程来容纳并处理越来越多的用户连接。在相同硬件配置的情况下,在这一模式下运行的web服务器的伸缩性要比运行在每个连接一个线程(thread-per-connection)模型下的好得多。

  在Philip McCarthy(Comet and Reverse Ajax的作者)的博客中,关于这两种线程模式的可伸缩性有一个很有意思的衡量基准(参见参考资料中的链接)。在图2中,你会发现同样的模式:在有太多连接时,线程模式会停止工作。

  图2. 线程模式的衡量基准

  每个连接一个线程模式(图2中的Threads)通常会有一个更好的响应时间,因为所有的线程都已启动、准备好且是等待中,但在连接的数目过高时,其会停止提供服务。在每个请求一个线程模式(图2中的Continuations)中,线程被用来为到达的请求提供服务,连接则是通过一个NIO选择器来处理。响应时间可能会较慢一些,但线程会回收再用,因此该方案在大容量连接方面有着更好的伸缩性。

  想要了解线程在幕后是如何工作的话,可以把一个LEGO™积木块想象成是选择器,每次传入的连接到达这一LEGO积木块时,其由一个管脚来标识。LEGO积木块/选择器有着与连接数一样多的管脚(一样多的键)。那么,只需要一个线程来等待新事件的发生,然后在这些管脚上遍历就可以了。当有事情发生时,选择器线程从发生的事件中检索出键值,然后就可以使用一个线程来为传入的请求提供服务。

  “Rox Java NIO Tutorial”这一教程有很好的使用Java中的NIO的例子(参见参考资料)。

  有请求作用域的服务

  许多框架都提供了服务或是过滤器(filter)来处理到达servlet的web请求,例如,某个过滤器会:

  1. 把JDBC连接绑定到某个请求线程上,这样整个请求就只用到一个连接。

  2. 在请求结束时提交所做的改变。

  另一个例子是Google Guice(一个依赖注入库)的Guice Servlet扩展。类似于Spring,Guice可把服务绑定在请求的作用域内,一个实例至多只会为每个新请求创建一次(参阅参考资料获得更多信息)。

  通常的做法包括了使用用户id来把从储存库中检索出来的用户对象缓存在请求中,用户id则是取自集群化的HTTP会话。在Google Guice中,你可能会有类似清单7中给出的代码。

  清单7. 请求作用域的绑定

@Provides
@RequestScoped
Member member(AuthManager authManager,
MemberRepository memberRepository) {
  return memberRepository.findById(authManager.getCurrentUserId());
}

  当某个member被注入到类中时,Guice会尝试这从请求中获取该对象,如果没有找到的话,它就会执行储存库调用并把结果放在请求中。

  请求作用域可与除了WebSocket之外的其他任何的反向Ajax解决方案一起使用,任何其他的依赖于HTTP请求的解决方案,无论是短的还是长的生存期的都可以,每个请求都会通过servlet分发系统,过滤器都会被执行。在完成一个暂停的(长生存其)HTTP请求时,你会在这一系列的后继部分中了解到还有另一种做法可让请求再次通过过滤器链。

  对于WebSocket来说,数据直接到达onMessage回调函数上,就像是在TCP套接口中的情况那样。不存在任何的HTTP请求送达这一数据,故也不存在获取或是存放作用域对象的请求上下文。因此在onMessage回调中使用需要作用域对象的服务就会失败。可下载源代码中的guice-and-websocket例子说明了如何绕过这一限制,以便仍然可在onMessage回调中使用请求作用域对象。当你运行这一例子,并在网页上点击每个按钮来测试一个Ajax调用(有请求作用域的)、一个WebSocket调用和一个使用了模拟请求作用域的WebSocket调用时,你会得到图3所示的输出。

  图3. 使用了请求作用域服务的WebSocket处理程序

  在使用下面任一种技术时,你可能都会遇到这些问题:

  1. Spring

  2. Hibernate

  3. 任何其他需要请求作用域或是每一请求模型的框架,比如说OpenSessionInViewFilter。

  4. 任何在过滤器的内部使用ThreadLocal这一设施来指定变量的作用域为请求线程并在以后访问这些变量的系统。

  Guice有一个优雅的解决方案,如清单8所示:

  清单8. 在WebSocket的onMessage回调中模拟一个请求作用域

// 在调用doWebSocketMethod时
// 保存到请求的引用
HttpServletRequest request = [...]
Map, Object> bindings =new HashMap, Object>();
// 我有一个服务需要一个请求来获取会话
// 因此我提供一个请求,但你可以提供任何其他
// 可能需要的绑定
bindings.put(Key.get(HttpServletRequest.class), request);
ServletScopes.scopeRequest(new Callable() {
  @Override
  public Object call() throws Exception {
  // 调用你的储存库或是任何用到作用域对象的服务
    outbound.sendMessage([...]);
    return null;
  }
}, bindings).call();

  暂停长生存期请求

  若使用Comet的话,还有另一障碍存在,那就是服务器端如何在不影响性能的情况下暂停一个长生存期请求,然后在服务器端事件到来时尽可能快地恢复并完成请求呢?

  很显然,你不能简单地让请求和响应停在那里,这会引发线程饥饿和高内存消耗。暂停非阻塞式的I/O中的一个长生存期请求,在Java中这需要一个特有的API。Servlet 3.0规范提供了这样的一个API(参见本系列的第1部分内容)。清单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();
// 把asyncContext的引用保存在某处

// 然后在需要的时候,在另一个线程中你可以恢复并完成
HttpServletResponse req =
(HttpServletResponse) asyncContext.getResponse();
req.getWriter().write("data");
req.setContentType([...]);
asyncContext.complete();

  在Servlet 3.0之前,每个容器都有着且现在仍有着自己的机制。Jetty的延续(continuation)就是一个很有名的例子;Java中的许多反向Ajax库都依赖于Jetty的continuation。其并非什么精彩绝伦的做法,也不需要你的应用运行在Jetty容器上。该API的聪明之处在于其能够检测出你正在运行的容器,如果是运行在另一个容器上,比如说Tomcat或是Grizzly,那么如果Servlet 3.0 API可用的话,就回退到Servlet 3.0 API上。这对于Comet来说没有问题,但如果你想要利用WebSocket的优势的话,目前别无选择,只能使用容器特有的功能。

  Servlet 3.0规范还没有发布,但许多容器都已经实现了这一API,因为这也是实施反向Ajax的一种标准做法。

  结束语

  WebSocket尽管存在一些不足之处,但却是一个功能非常强大的反向Ajax解决方案。其目前还未在所有浏览器上实现,且如果没有反向Ajax库的帮助的话,在Java服务器端并不容易使用。因为你使用的不是标准的请求-响应风格,所有你不能依赖过滤器链的作用域执行。Comet和WebSocket需要服务器端的容器特定功能,因此在使用新出的容器时,你需要注意一下,它可能没有做这方面的扩充。

  请继续关注这一系列的第3部分,该部分内容将探讨用于Comet和WebSocket的不同的服务器端API,你还可了解到Atomsphere,这是一个反向Ajax框架。

  下载

  描述        名称         大小   下载方法

  文章的源代码   reverse_ajaxpt2_source.zip 14KB   HTTP

  参考资料

  1. “Start using HTML5 WebSockets today”(Nettuts+):重温在PHP中如何运行一个WebSocket服务器,并考虑如何构建一个客户端来通过WebSocket协议发送和接收消息。

  2. “The WebSocket API”(W3C, July 2011):这一规范定义的API使得网页能够使用WebSocket协议来和远程主机进行双向通信。

  3. jWebSocket支持的浏览器:了解jWebSocket和Flash套接口桥所支持浏览器的各方面信息。

  4. 了解更多关于Servlet 3.0对异步处理的支持方面的内容。

  5. Philip McCarthy的博客上的文章Comet & Java: Threaded Vs Nonblocking I/O中有着更多的内容。

  6. The Rox Java NIO Tutorial这一教程收集了作者使用Java NIO库的一些经验,以及几十条的诀窍、技巧、建议和充斥着互联网的告诫做法。

  7. 在维基百科上了解这些内容:

  7.1 Ajax
  7.2 Reverse Ajax
  7.3 Comet
  7.4 WebSockets

  8. “Exploring Reverse AJAX”(Google Maps .Net Control博客,2006年8月):获得一些关于反向Ajax技术的介绍说明。

  9. “Cross-domain communications with JSONP, Part 1: Combine JSONP and jQuery to quickly build powerful mashups”(developerWorks, February 2009):了解如何把不起眼的跨域调用技术(JSONP)和一个灵活的JavaScript库(JQuery)结合在一起,以令人惊讶的速度构建出一些功能强大的聚合应用。

  10. “Cross-Origin Resource Sharing (CORS)”规范(W3C, July 2010):了解更多关于这一机制的内容,该机制允许XHR执行跨域请求。

  11. “Build Ajax applications with Ext JS”(developerWorks, July 2008):对大大增强了JavaScript开发的这一框架有一个大概的了解。

  12. “Compare JavaScript frameworks”(developerWorks, February 2010):对极大地增强了JavaScript开发的那些框架有一个整体的了解。

  13. “Mastering Ajax, Part 2: Make asynchronous requests with JavaScript and Ajax”(developerWorks, January 2006):学习如何使用Ajax和XMLHttpRequest对象来创建一种永不会让用户等待服务器响应的请求/响应模型。

  14. “Create Ajax applications for the mobile Web”(developerWorks, March 2010):了解如何使用Ajax构建跨浏览器的智能手机Web应用。

  15. “Where and when to use Ajax in your applications”(developerWorks, February 2008):了解如何使用Ajax来改进网站,同时避免糟糕的用户体验。

  16. “Improve the performance of Web 2.0 applications“(developerWorks, December 2009):探讨不同的浏览器端缓存机制。

  17. “Introducing JSON”(JSON.org):获得对JSON语法的一个入门介绍。

  18. developerWorks Web development zone:获得各种谈论基于Web的解决方案的文章。

  19. developerWorks podcasts:收听各种与软件开发者进行的有趣的访谈和讨论。

  20. developerWorks technical events and webcasts:随时关注developerWorks的技术事件和webcast的进展。

  获取产品和技术

  1. WebSocketJS(WebSocket Flash Bridge):获取这一由Flash支持的HTML5 WebSocket实现。

  2. Google Guice:获取Google Guice,一个Java 5及以上版本的轻量级的依赖注入框架。

  3. Jetty:获取Jetty,一个web服务器和javax.servlet容器,外带对WebSocket的支持。

  4. Apache Maven:获取Maven,一个软件项目管理和包容工具。

  5. Java Development Kit, Version 6:获得Java平台标准版(Java Platform, Standard Edition,Java SE),该平台允许你在台式机和服务器上,以及在当今要求苛刻的嵌入式环境上开发和部署Java应用。

  6. 免费试用IBM软件,下载使用版,登录在线试用,在沙箱环境中使用产品,或是通过云来访问,有超过100种IBM产品试用版选择。

  讨论

  1. 现在就创建你的developerWorks个人资料,并设置一个关于Reverse Ajax的观看列表。与developerWorks社区建立联系并保持联系。

  2. 找到其他在web开发方面感兴趣的developerWorks成员

  3. 分享你的知识:加入一个关注web专题的developerWorks组

  4. Roland Barcia在他的博客中谈论Web 2.0和中间件

  5. 关注developerWork成员的shared bookmarks on web topics

  6. 快速获得答案:访问Web 2.0 Apps论坛

  7. 快速获得答案:访问Ajax论坛

 

posted @ 2017-04-21 14:11  TyrantMaster  阅读(567)  评论(0编辑  收藏  举报