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

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

前言web


web开发在过去的几年中有了很大的进展,咱们已经远超了把静态网页连接在一块儿的作法,这种作法会引发浏览器的刷新,而且要等待页面的加载。如今须要的是可以经过web来访问的彻底动态的应用,这些应用一般须要尽量的快,提供近乎实时的组件。在这一新的由五部分组成的文章系列中,咱们学习如何使用反向Ajax(Reverse Ajax)技术来开发事件驱动的web应用。ajax

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

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中看到这一例子的做用。app

反向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 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实现的轮询的优势和缺点:

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 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

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

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标签来处理的,所以你没有办法知道链接什么时候在哪一端已被断开了。

多部分的XMLHttpRequest 

第二种技术,更可靠一些,是在XMLHttpRequest对象上使用某些浏览器(好比说Firefox)支持的multi-part标志。Ajax请求被发送给服务器端并保持打开状态,每次有事件到来时,一个多部分的响应就会经过这同一链接来写入,清单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展现了如何挂起一个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。

请继续阅读这一系列的第2部分:该部分将会探讨第三种反向Ajax技术:WebSocket。尽管还不是全部的浏览器都支持该技术,但WebSocket确定是一种很是好的反向Ajax通讯媒介,WebSocket消除了全部与HTTP链接的无状态特性相关的限制。第2部分还会谈及由Comet和WebSocket技术带来的服务器端约束。

相关文章
相关标签/搜索