Multipart/x-mixed-replace示例... 11
Comet概述
定义
Comet是基于Http长连接的新一代互联网技术,对其较早的定义来自http://alex.dojotoolkit.org/?p=545。
要讲Comet,先从Server Push开始。
Server Push是一种很早就存在的技术,以前在实现上主要是通过客户端的套接字,或是服务器端的远程调用。因为浏览器技术的发展比较缓慢,没有为Server Push的实现提供很好的支持,在纯浏览器的应用中很难有一个完善的方案去实现Server Push并用于商业程序。最近几年,因为 AJAX 技术的普及,以及把 IFrame 嵌在“htmlfile“的 ActiveX 组件中可以解决 IE 的加载显示问题,一些受欢迎的应用如 meebo,gmail+gtalk 在实现中使用了这些新技术;同时“服务器推”在现实应用中确实存在很多需求。因为这些原因,基于纯浏览器的“服务器推”技术开始受到较多关注,Alex Russell(Dojo Toolkit 的项目 Lead)称这种基于 HTTP 长连接、无须在浏览器端安装插件的Server Push技术为“Comet”。目前已经出现了一些成熟的 Comet 应用以及各种开源框架;一些 Web 服务器如 Jetty 也在为支持大量并发的长连接进行了很多改进。关于 Comet 技术最新的发展状况请参考关于 Comet 的 wiki.
Wikipedia上对Comet的定义如下:
Comet is a World Wide Web application architecture in which a web server sends data to a client program (normally a web browser) asynchronously without any need for the client to explicitly request it. It allows creation of event-driven web applications, enabling real-time interaction otherwise impossible in a browser. Though the term Comet was coined in 2006,the idea is several years older, and has been called various names, including server push, HTTP push, HTTP streaming, Pushlets, Reverse Ajax, and others.
Comet applications use long-lived HTTP connections between the client and server, which the server can respond to lazily, pushing new data to the client as it becomes available. This differs from the original model of the web, in which a browser receives a complete web page in response to each request, and also from the Ajax model, in which browsers request chunks of data, used to update the current page. The effect is similar to applications using traditional Ajax with polling to detect new information on the server, but throughput is improved and latency and server load are decreased.
Comet does not refer to any specific technique for achieving this user-interaction model, but encompasses all of them—though it implies the use of browser-native technologies such as JavaScript as opposed to proprietary plugins. Several such techniques exist, with various trade-offs in terms of browser support and side effects, latency, and throughput.
传统的web应用都是基于请求-响应的模式,ajax的改进只是非全页面更新,无法解决实时性和事件驱动。ajax with polling通过定时请求可以实现伪实时,但频繁的建立和销毁连接又会耗费服务器大量资源,增加带宽使用。Comet使用Http1.1 长连接,实现实时的服务器-客户端数据推送。Comet的实现可以有两种方式,Streaming和Long-Polling。Streaming方式建立连接后,两端均不断开,使用此连接实时传输消息。Long-Polling方式一旦完成数据接收,即断开当前连接并重新建立新连接。二者相比Streaming性能最优,但即使是Long-Polling,不管是服务端负载还是对网络带宽的使用,也大大优于传统的Polling。
典型应用场景
Comet特别适用于需要和服务器端实时交互的应用,如聊天,远程协作等类型的Web应用。
浏览器端兼容性
在Comet之前,可通过下述途径来实现类似的效果,其中部分方式现在仍有较多使用。各方式对浏览器端的兼容性如下:
1. IFrame
最早的实现方式,通过隐藏的IFrame元素实现对服务器的持续请求,在返回内容中输出并执行JavaScript以实现。优点是基本所有浏览器都支持,缺点是会导致部分浏览器的状态条一直为读取状态且鼠标状态为忙碌,影响用户体验。
2. Htmlfile ActiveX object
通过将HTML IFrame元素置于一个ActiveX中,规避了1中所提到的两个缺点。其缺点很显然,仅IE支持,如Google Gtalk。另外Zeitoun的comet-iframe将提供Javascript对象支持内嵌IFrame,支持IE,FF。
3. Multipart XHR(XMLHttpRequest)
1995年,Netscape中增加了一个特性叫Server Push,这个特性允许服务器端在同一个Http Response中发送图片或HTML页的新版本到浏览器端,通过在HTML头声明ContentType为multipart/x-mixed-replace实现,浏览器会使用新版本的HTML替换已有页面。不过这个特性仅在使用Gecko内核的浏览器中支持。
4. XHR streaming
通过自定义返回数据格式,在浏览器端捕获onreadystatechange 事件并在readyState=3时回调对应JavaScript方法来处理数据并实现。此方式IE不可行。
5. XHR long polling
上述若干方法兼容性都不是很好,而XHR long polling在所有支持XHR的浏览器中都可以使用。其实现方式为:浏览器建立一个异步连接,当服务器响应后回调JS方法,然后重新建立新的连接并等待服务器端下一次响应。
6. Dynamic script tag long polling
该方式更好解决跨域调用问题,虽然跨域页面的互操作也可以通过代理来解决。
7. Server-sent events
HTML5草案中的新元素event source。
可靠性
代理服务器和防火墙可能对Comet应用有不利影响。一般防火墙都会断开已建立时间过长的连接。大多数Comet框架的解决办法是重建连接。
另外,代理服务器可能缓存被推送的数据,使应用丧失实时性。
可扩展性
因为Comet应用实时发送服务器端事件,一般会比其他的传统web应用消耗更多资源,使得其扩展性相对较差。
首先,Comet应用需要在服务器和浏览器间维持至少一个长连接,而传统的web服务器按照页面-页面请求进行的设计和优化使其在无法在同一时间维持如此多的连接。这导致Comet应用无法在一个web server上处理大量请求,垂直扩展性差。
垂直扩展性差的原因在于传统的web server如Apache等的处理线程使用同步IO。每个请求被分配一个线程去处理,并尽可能尽快处理完毕并关闭连接以处理下一个请求。但Comet应用建立的连接是持久的,处理请求的每个线程不能用于处理其他请求,如果有大量长连接建立,web server就可能无法处理新发起的请求。
Jetty和Tomcat等Web应用服务器已为Comet应用进行优化(ContinuationServlet, 但是因为不是标准的servlet实现,不具备服务器兼容性),使用异步IO(JDK5之后)来处理长连接,一定数目的线程去轮询各个连接并在到达每个连接时将所需数据推送到浏览器端。此类服务器不仅可以处理比线程服务器多的多的长连接,而且随着连接数增加,处理延迟被均摊到每个连接,对用户影响小。值得注意的是,sun新提交的JSR315 servlet3.0也包括了comet,今后将成为J2EE服务器的标准实现。
另外,一般Comet应用都是交互性的,允许多用户间通讯,因此将处理任务分派到多个server去处理有困难。即水平扩展性差。
对水平扩展性的一种解决方式是按照事件对服务器进行分组,将订阅某组事件的请求转发到指定的服务器上。
注意事项
1. HTTP1.1标准中有如下规定:“A single-user client SHOULD NOT maintain more than 2 connections with any server or proxy”,并且此规则被包括IE和FF在内的绝大多数浏览器遵守。因此,当comet占用一个http长连接,将可能导致浏览器不能为ajax请求创建新的连接,比如浏览器正在读取大量图片时。
2. 如果有新的公共数据需要push,所有servlet要同时将数据push出去,在push之前还会有查询,这样对cpu内存和网络都会有较大消耗,而且是同时的,会造成共振效应。
可以考虑使用一个公共的查询线程来负责,request被放入一个队列,查询线程负责轮询所有request,来将各自需要的数据push到client,这样可以响应的并发连接数可能会多很多,且避免了共振。
其他解决方案
鉴于浏览器对Comet的直接支持较差,有部分开发者使用了flash和java applet等浏览器插件来实现实时响应。只要安装了相应插件,该方式不用考虑浏览器的差异,通用性较好,且不用占用HTTP连接。
Comet与Ajax的区别
请见下图。
NIO
对比
以Jetty的ContinousServlet(使用NIO)和标准Servlet性能对比来说明:
代码段I:BlockingServlet(未使用NIO):
public class BlockingServlet extends HttpServlet {
public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {
String reqId = req.getParameter("id");
res.setContentType("text/plain");
res.getWriter().println("Request: "+reqId+"/tstart:/t" + new Date());
res.getWriter().flush();
try {
Thread.sleep(2000);
} catch (Exception e) {}
res.getWriter().println("Request: "+reqId+"/tend:/t" + new Date());
}
}
现在可以观察到 servlet 响应一些同步请求的行为。展示了控制台输出,五个使用 lynx 的并行请求。命令行启动五个 lynx 进程,将标识序号附加在请求 URL 的后面。
输出I:对 BlockingServlet 并发请求的输出:
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/blocking?id=$i & done
Request: 1 start: Sun Jul 01 12:32:29 BST 2007
Request: 1 end: Sun Jul 01 12:32:31 BST 2007
Request: 2 start: Sun Jul 01 12:32:31 BST 2007
Request: 2 end: Sun Jul 01 12:32:33 BST 2007
Request: 3 start: Sun Jul 01 12:32:33 BST 2007
Request: 3 end: Sun Jul 01 12:32:35 BST 2007
Request: 4 start: Sun Jul 01 12:32:35 BST 2007
Request: 4 end: Sun Jul 01 12:32:37 BST 2007
Request: 5 start: Sun Jul 01 12:32:37 BST 2007
Request: 5 end: Sun Jul 01 12:32:39 BST 2007
输出I和预期一样。因为 Jetty 只可以使用一个线程执行 servlet 的 service() 方法。Jetty 对请求进行排列,并按顺序提供服务。当针对某请求发出响应后将立即显示时间戳(一个 end 消息),servlet 接着处理下一个请求(后续的 start 消息)。因此即使同时发出五个请求,其中一个请求必须等待 8 秒钟的时间才能接受 servlet 处理。
请注意,当 servlet 被阻塞时,执行任何操作都无济于事。这段代码模拟了请求等待来自应用程序不同部分的异步事件。这里使用的服务器既不是 CPU 密集型也不是 I/O 密集型:只有线程池耗尽之后才会对请求进行排队。
现在,查看 Jetty 6 的 Continuations 特性如何为这类情形提供帮助。代码段II展示了代码段I中使用 Continuations API 重写后的 BlockingServlet。
代码段II:. ContinuationServlet
public class ContinuationServlet extends HttpServlet {
public void service(HttpServletRequest req, HttpServletResponse res)
throws java.io.IOException {
String reqId = req.getParameter("id");
Continuation cc = ContinuationSupport.getContinuation(req,null);
res.setContentType("text/plain");
res.getWriter().println("Request: "+reqId+"/tstart:/t"+new Date());
res.getWriter().flush();
cc.suspend(2000);
res.getWriter().println("Request: "+reqId+"/tend:/t"+new Date());
}
}
输出II: 对 ContinuationServlet 的五个并发请求的输出
$ for i in 'seq 1 5' ; do lynx -dump localhost:8080/continuation?id=$i & done
Request: 1 start: Sun Jul 01 13:37:37 BST 2007
Request: 1 start: Sun Jul 01 13:37:39 BST 2007
Request: 1 end: Sun Jul 01 13:37:39 BST 2007
Request: 3 start: Sun Jul 01 13:37:37 BST 2007
Request: 3 start: Sun Jul 01 13:37:39 BST 2007
Request: 3 end: Sun Jul 01 13:37:39 BST 2007
Request: 2 start: Sun Jul 01 13:37:37 BST 2007
Request: 2 start: Sun Jul 01 13:37:39 BST 2007
Request: 2 end: Sun Jul 01 13:37:39 BST 2007
Request: 5 start: Sun Jul 01 13:37:37 BST 2007
Request: 5 start: Sun Jul 01 13:37:39 BST 2007
Request: 5 end: Sun Jul 01 13:37:39 BST 2007
Request: 4 start: Sun Jul 01 13:37:37 BST 2007
Request: 4 start: Sun Jul 01 13:37:39 BST 2007
Request: 4 end: Sun Jul 01 13:37:39 BST 2007
输出II中有两处需要重点注意。首先,每个 start 消息出现两次;先不要着急。其次,更重要的一点,请求现在不需排队就能够并发处理,注意所有 start 和 end 消息的时间戳是相同的。因此,每个请求的处理时间不会超过两秒,即使只运行一个 servlet 线程。
支持
现有Jetty 6.1.6 和Tomcat6以上版本提供非标准的NIO Servlet实现。未来Servlet 3.0标准中将包含对标准实现的规范。
性能对比
性能对比使用参考资料中的部分内容,原文摘抄如下:
The following table shows that a Web 1.0 server can handle 10000 users with 500 threads and 36MB of thread stacks, which is easily achievable with current JVMs and servers. For a Web 2.0 application these requirements explode an order of magnitude to 10600 threads and 694MB of stack memory, which is pushing the limits of current servers without even considering the resource requirements of the application.Web 2.0 Comet applications can be implemented with continuations with only a modest increase in the server requirements:
| Formula | Web 1.0 | Web 2.0 + Comet | Web 2.0 + Comet + Continuations |
Users | u | 10000 | 10000 | 10000 |
|
|
|
|
|
Requests/Burst | b | 5 | 2 | 2 |
Burst period (s) | p | 20 | 5 | 5 |
Request Duration (s) | d | 0.200 | 0.150 | 0.175 |
Poll Duration (s) | D | 0 | 10 | 10 |
|
|
|
|
|
Request rate (req/s) | rr=u*b/20 | 2500 | 4000 | 4000 |
Poll rate (req/s) | pr=u/d | 0 | 1000 | 1000 |
Total (req/s) | r=rr+pr | 2500 | 5000 | 5000 |
|
|
|
|
|
Concurrent requests | c=rr*d+pr*D | 500 | 10600 | 10700 |
Min Threads | T=c T=r*d | 500 - | 10600 - | - 875 |
Stack memory | S=64*1024*T | 32MB | 694MB | 57MB |
A项目中Comet的使用
在A项目中根据Bayeux协议实现进行封装和修改,完成A功能需求。
在后台代码中,通过增加消息体内容的类型定义,覆写Bayeux协议中定义的事件回调函数,实现A的数据发送:
l 覆写subscribe回调函数,实现对订阅者的缓存,并对新用户进行历史留言发送
l 覆写publish回调函数,实现IP屏蔽、屏蔽词过滤、已发布消息缓存、更新访谈最高在线人数和消息发布的操作;
l 覆写协议实现的默认初始化函数,实现频道创建、订阅和发布的安全验证;
l 增加消息体中IP地址、发布时间、发言类型、用户数量等的定义,并在消息接收Servlet和后台发布中对原始消息体进行封装和补充。
为了保证效率,后台程序尽量使用缓存和线程轮询,通过JDK语法和Concurrent包的工具类对竞争资源进行同步。
前台系统,使用Dojo对JSON消息体进行封装发送和解析,优化历史消息显示策略,实时进行新消息发送和接收。
l 创建JavaScript类,实现对页面操作的封装。
l 对不同的访谈定义不同的频道编号,并分别生成各自静态页,便于今后的服务拆分和管理。
l 为不影响前台浏览器效率,对接收到的消息进行缓存,批量进行页面操作,避免频繁操作(700次/S)导致浏览器CPU占用率过高。
Dojo和Bayeux简介
Dojo是一套重量级的,强大的面向对象的JavaScript库,包括dojo和dojox两大部分。为开发富客户端应用提供了一套现成的工具库。
DWR也已加入Dojo,Struts2也整合了dojo,且很多IDE也提供过了对DOJO的支持和良好的调试环境,包括Eclipse WTP+ATF,Netbeans5以上。
Bayeux是Dojo基金会定义的一个协议,应Comet应用的需要而产生,其目标是消除不同WebServer的Comet实现间的差异,以及不同浏览器脚本使用Comet的差异并提高已有代码的重用率。Bayeux是第一个比较全面的实现Comet的协议。特别是对long-polling,callback-polling,iframe这几种comet的实现手段都能支持.
Bayeux使用JSON作为数据封装格式,使用long polling方式获取服务端数据,以publish/subscribe方式发布和订阅消息。
备注
Multipart/x-mixed-replace示例
<%
response.setContentType("multipart/mixed;boundary=BOUNDARY");
int i = 1;
while(i>0) {
out.println("--BOUNDARY/r/n");
%>
<html>
<head><title>百度一下,你就知道 </title>
</head>
<body>
<%
out.println("push "+i+"<br/>");
out.flush();
try {
Thread.currentThread().sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
%>
</body>
</html>
<%
i++;
}
%>
Pushlet简介
Pushlet 是一个开源的 Comet 框架,在设计上有很多值得借鉴的地方,对于开发轻量级的 Comet 应用很有参考价值。
Pushlet 使用了观察者模型:客户端发送请求,订阅感兴趣的事件;服务器端为每个客户端分配一个会话 ID 作为标记,事件源会把新产生的事件以多播的方式发送到订阅者的事件队列里。
pushlet 提供了基于 AJAX 的 JavaScript 库文件用于实现长轮询方式的“服务器推”;还提供了基于 iframe 的 JavaScript 库文件用于实现流方式的“服务器推”。
JavaScript 库做了很多封装工作:
定义客户端的通信状态:STATE_ERROR、STATE_ABORT、STATE_NULL、STATE_READY、STATE_JOINED、STATE_LISTENING;
保存服务器分配的会话 ID,在建立连接之后的每次请求中会附上会话 ID 表明身份;
提供了 join()、leave()、subscribe()、 unsubsribe()、listen() 等 API 供页面调用;
提供了处理响应的 JavaScript 函数接口 onData()、onEvent()…
网页可以很方便地使用这两个 JavaScript 库文件封装的 API 与服务器进行通信。
客户端与服务器端通信信息格式:
pushlet 定义了一套客户与服务器通信的信息格式,使用 XML 格式。定义了客户端发送请求的类型:join、leave、subscribe、unsubscribe、listen、refresh;以及响应的事件类型:data、join_ack、listen_ack、refresh、heartbeat、error、abort、subscribe_ack、unsubscribe_ack。
服务器端事件队列管理:
pushlet 在服务器端使用 Java Servlet 实现,其数据结构的设计框架仍可适用于 PHP、C 编写的后台客户端。
Pushlet 支持客户端自己选择使用流、拉(长轮询)、轮询方式。服务器端根据客户选择的方式在读取事件队列(fetchEvents)时进行不同的处理。“轮询”模式下 fetchEvents() 会马上返回。”流“和”拉“模式使用阻塞的方式读事件,如果超时,会发给客户端发送一个没有新信息收到的“heartbeat“事件,如果是“拉”模式,会把“heartbeat”与“refresh”事件一起传给客户端,通知客户端重新发出请求、建立连接。
客户服务器之间的会话管理:
服务端在客户端发送 join 请求时,会为客户端分配一个会话 ID, 并传给客户端,然后客户端就通过此会话 ID 标明身份发出 subscribe 和 listen 请求。服务器端会为每个会话维护一个订阅的主题集合、事件队列。
服务器端的事件源会把新产生的事件以多播的方式发送到每个会话(即订阅者)的事件队列里。
参考资料
1. http://www.javaeye.com/topic/28020?page=1
2. http://www.ibm.com/developerworks/cn/web/wa-lo-comet/index.html
3. http://docs.codehaus.org/display/JETTY/Continuations
4. http://www.pushlets.com/doc/protocol-all.html
5. http://www.matrix.org.cn/resource/article/2007-01-16/bcc2c490-a502-11db-8440-755941c7293d.html
6. http://ajaxpatterns.org/HTTP_Streaming //usefull
7. http://tomcat.apache.org/tomcat-6.0-doc/aio.html //advanced IO & comet
8. http://www.duduwolf.com/wiki/2007/440.html
9. https://grizzly.dev.java.net/ //Grizzly is an NIO frameowork for building scalable application(NIO和IO的主要区别是非阻塞的)
10. http://www.ibm.com/developerworks/cn/java/j-jettydwr/index.html //jetty ContinuationServlet 简介 和dwr reverse ajax
11. http://alex.dojotoolkit.org/?p=545 //Comet: Low Latency Data for the Browser
12. http://www.zeitoun.net/index.php?2007/06/22/46-how-to-implement-comet-with-php // a php comet demo
Comet讨论
- Joe Walker (23 October 2007). “Why Comet is of Growing Importance”. Comet Daily. Retrieved 4 Dec 2007.
- Rodney Gedda (14 October 2005). “Desktop apps coming to the Web: Google”. Computerworld. Retrieved 29 Nov 2007.
- Paul Graham (November 2005). “Web 2.0”. paulgraham.com. Retrieved 29 Nov 2007.
- Alex Russell (3 March 2006). “Comet: Low Latency Data for the Browser”. Continuing Intermittent Incoherency. Retrieved 29 Nov 2007.
- Greg Wilkins (6 November 2007). “Comet is Always Better Than Polling”. Comet Daily. Retrieved 29 Nov 2007.
- Jacob Rus (11 December 2007). “The Future of Comet: Part 1, Comet Today”. Comet Daily. Retrieved 14 Dec 2007.
- Andrew Betts (2007). “Browser Techniques”. Meteor website. Retrieved 29 Nov 2007.
- Maciej Stachowiak (26 June 2007). “Re: XMLHttpRequest and readyState==3”. Webkit-dev mailing list. Retrieved 29 Nov 2007.
- Alex Russell (12 February 2006). “What else is buried down in the depths of Google’s amazing JavaScript?”. Continuing Intermittent Incoherency. Retrieved 29 Nov 2007.
For advice about avoiding garbage collection bugs when implementing Comet with htmlfiles, see:
Michael Carter (25 October 2007). “HTTP Streaming and Internet Explorer”. Comet Daily. Retrieved 29 Nov 2007.
Michael Carter (18 November 2007). “IE ActiveX("htmlfile") Transport, Part II”. Comet Daily. Retrieved 29 Nov 2007. - Johnny Stenback, et al. (March–April 2004). “Bug 237319 – Add support for server push using
multipart/x-mixed-replace
with XMLHttpRequest”. Mozilla Bugzilla bug tracker. Retrieved 29 November 2007. Also see:
Alex Russell (6 August 2005). “Toward server-sent data w/o iframes”. Continuing Intermittent Incoherency. Retrieved 29 Nov 2007. - Rob Butler, et al. (June 2006). “Bug 14392: Add support for
multipart/x-mixed-replace
to XMLHttpRequest”. Webkit Bugzilla bug tracker. Retrieved 29 Nov 2007. - Alex Russell (21 December 2006). “Adventures In Comet and Multipart Mime”. Continuing Intermittent Incoherency. Retrieved 29 Nov 2007.
- Andrew Betts (4 December 2007). “Cross Site Scripting Joy”. Comet Daily. Retrieved 14 Dec 2007.
- Bob Ippolito (5 December 2005). “Remote JSON – JSONP”. from __future__ import *. Retrieved 30 Nov 2007.
- Alex Russell (22 July 2006). “Cross Domain Comet”. Continuing Intermittent Incoherency. Retrieved 30 Nov 2007.
- HTTP 1.1 specification, section 8.14. W 3C . Retrieved 30 Nov 2007.
- Alessandro Alinone (December 2005). “Changing the Web Paradigm”. Lightstreamer white paper. Retrieved 14 Dec 2007.
- Michael Carter (24 October 2007). “Comet for Highly Scalable Applications”. Presentation at The Ajax Experience Boston . (Link goes to PDF slides). Retrieved 30 Nov 2007.
- Alessandro Alinone (19 October 2007). “Comet and Push Technology”. Comet Daily. Retrieved 14 Dec 2007.
- Just Van Den Broecke (3 January 2007). “Pushlets: Send events from servlets to DHTML client browsers”. JavaWorld. Retrieved 14 Dec 2007.
- Dion Almaer (29 September 2005). “Jotspot Live: Live, group note-taking” (interview with Abe Fettig). Ajaxian. Retrieved 15 Dec 2007.
Matt Marshall (15 December 2006). “Renkoo launches event service — in time to schedule holiday cocktails”. Venture Beat. Retrieved 15 Dec 2007. - Alex Russell, et al. (2007). Bayeux Protocol specification, 1.0 draft 1. Dojo Foundation. Retrieved 14 Dec 2007.
- Jesse James Garrett (18 February 2005). “Ajax: A New Approach to Web Applications”. Adaptive Path. Retrieved 29 Nov 2007.
进阶资料
- Ian Hickson, et al. (2005–2007). HTML 5 specification, Section 6.2: Server-sent DOM events. WHATWG. Retrieved 14 Dec 2007
- Rohit Khare (August 2005). “Beyond AJAX: Accelerating Web Applications with Real-Time Event Notification”. KnowNow white paper (link from the Wayback Machine). Retrieved 14 Dec 2007