反向 Ajax,第 1 部分: Comet 简介

简介

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

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

 

 

Ajax、反向 Ajax 和 WebSockets

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

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

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

使用 HTTP 轮询的反向 Ajax

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

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

清单 2. 清单 2 JavaScript 轮询
setInterval(function() { 
    $.getJSON('events', function(events) { 
        console.log(events); 
    }); 
}, 2000);

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

清单 3. 清单 3 轮询演示例子的输出
[client] checking for events... 
[client] no event 
[client] checking for events... 
[client] 2 events 
[event] At Sun Jun 05 15:17:14 EDT 2011 
[event] At Sun Jun 05 15:17:14 EDT 2011 
[client] checking for events... 
[client] 1 events 
[event] At Sun Jun 05 15:17:16 EDT 2011

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

  • 优点:很容易实现,不需要任何服务器端的特定功能,且在所有的浏览器上都能工作。
  • 缺点:这种方法很少被用到,因为它是完全不具伸缩性的。试想一下,在 100 个客户端每个都发出 2 秒钟的轮询请求的情况下,所损失的带宽和资源数量,在这种情况下 30% 的请求没有返回数据。

Piggyback

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

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

该图描述了客户端发送一个 POST 请求但是得到混合响应。服务器端没有发生任何事件,所以混合响应只包含请求的响应。然后有两个事件到达服务器端,但是它们需要等待下一个客户端请求,这样混合响应会同时包含这两个事件和正常的请求响应。如果客户端不触发任何动作,那么将不会检索到达服务器端的事件。

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

清单 4. 清单 4 piggyback 代码示例
$('#submit').click(function() { 
    $.post('ajax', function(data) { 
        var valid = data.formValid; 
        // process validation results 
        // then process the other part of the response (events) 
        processEvents(data.events); 
    }); 
});

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

清单 5. 清单 5 piggyback 输出示例
[client] checking for events... 
[server] form valid ? true 
[client] 4 events 
[event] At Sun Jun 05 16:08:32 EDT 2011 
[event] At Sun Jun 05 16:08:34 EDT 2011 
[event] At Sun Jun 05 16:08:34 EDT 2011 
[event] At Sun Jun 05 16:08:37 EDT 2011

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

  • 优点:没有不返回数据的请求,因为客户端对何时发送请求做了控制,对资源的消耗较少。该方法也是可用在所有的浏览器上,不需要服务器端的特殊功能。
  • 缺点:当累积在服务器端的事件需要传送给客户端时,您却一点都不知道,因为这需要一个客户端行为来请求它们

Comet

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

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

图 3. 图 3.使用 Comet 的反向 Ajax

使用 Comet 的反向 Ajax

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

 

使用 HTTP 流的 Comet

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

Forever Iframes

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

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

多部分的 XMLHttpRequest

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

清单 6. 清单 6 设置多部分流请求的 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清单 7 展示了如何挂起一个 HTTP 流请求。(本系列的第 3 部分会更加详细地谈及这些 API。)

清单 7. 清单 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 所示:

清单 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 所示:

图 4. HTTP 流请求的 Firebug 视图

HTTP 流请求的 FireBug 视图的屏幕截图。Firefox 中的 FireBug 插件显示了相同 Ajax 流请求中的连续响应。

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

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

使用 HTTP 长轮询的 Comet

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

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

script 标签

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

  • 优点:因为是基于 HTML 标签的,所有这一技术非常容易实现,且可跨域工作(默认情况下,XMLHttpRequest 不允许向其他域或是子域发送请求)。
  • 缺点:类似于 iframe 技术,错误处理缺失,您不能获得连接的状态或是有干涉连接的能力。

XMLHttpRequest 长轮询

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

清单 9. 清单 9 设置长轮询请求的 JavaScript 代码示例
function long_polling() { 
    $.getJSON('ajax', function(events) { 
        processEvents(events); 
        long_polling(); 
    }); 
} 

long_polling();

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

清单 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 所示:

清单 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 命令来运行它。

  • 优点:客户端很容易实现良好的错误处理系统和超时管理。这一可靠的技术还允许在与服务器端的连接之间有一个往返,即使连接是非持久的(当您的应用有许多的客户端时,这是一件好事)。它可用在所有的浏览器上;您只需要确保所用的 XMLHttpRequest 对象发送到了简单的 Ajax 请求就可以了。
  • 缺点:相比于其他技术来说,不存在什么重要的缺点,像所有我们已经讨论过的技术一样,该方法依然依赖于无状态的 HTTP 连接,其要求服务器端有特殊的功能来临时挂起连接。

猜你喜欢

转载自aoyouzi.iteye.com/blog/2311710