今天见到sinopf发表了一篇关于所谓的服务器推技术(帖见:http://bbs.phpchina.com/thread-100663-1-1.html),其实是引人耳目的东西。我这里没有任何褒贬sinopf这个人的意思,我只是看了他的文章后,发现他那个根本不是服务器推技术,为了给大家解释清楚,我就写了这篇教程。
初次见到“服务器推”这个名词,我相信你和我一样,都是一头雾水,不知道什么意思。为了让你更好的理解什么叫做“服务器推”,我来举个简单的例子。
在很久很久以前,出现了互联网,这种基于B/S(浏览器/服务器)的工作流程是这样的。比如你打开了一个网站:http://www.skiyo.cn这里你得注意到,这个动作是人为的,人主动发出请求到浏览器,浏览器再把请求传送给服务器,也就是http://www.skiyo.cn所在的服务器得到相应,把网站内容以数据包的形式传送给浏览器,浏览器进行解析最终得到我们想看到的页面。
到后来,有种技术横空出世,它就是大名鼎鼎的AJAX。但是它并没有从本质上改变这种传统的浏览方式,然而它也是种进步,因为它是异步的http请求。可以这样简单的理解,你还是打开我的网站:http://www.skiyo.cn结果你看完了我第一页的所有文章,当你点击页数“2”的时候,你对浏览器有个请求,这个请求通过JavaScript发送给了服务器,这里的一个重要的特性就是页面的局部请求,JavaScript发送的请求不是要求更新全页面,而只是更新了文章列表,这就是AJAX。但是这种方式还需要浏览器主动地向服务器发送请求。你可以参考下图进行一下传统的 Web 应用模型与基于 AJAX 的模型之比较
到这里,我们可以考虑来写一个简单的聊天室了。这个页面布局非常简单,上面的一个div我们用来放聊天的内容,比如某某某说你好等等,下面有一个输入框一个按钮,用来说话。服务器端也好处理,弄个PHP显示聊天列表,弄个PHP用来接收聊天信息写到数据库中。这样一个简单的聊天室模型就出来了,但是还有让人最头痛的事,那就是如何实时的显示聊天内容呢?
在win32的socket编程中,比如QQ,我们可以用阻塞的方式等待消息的到来,但是在web编程就没这么简单了,你想到什么解决办法?我比较笨,只想到了AJAX。
写一个简单的JavaScript函数,用来异步获取聊天的内容,然后再写个setTimeout方法,调用刚才的AJAX请求,就设置为1秒请求一次。
这种方法虽然简单,但是不但客户端负荷大服务器的负荷也不小,再想想还有什么方法呢?
这种情况下“服务器推”这种技术就非常有用了,看完上面的介绍,相信你已经从字面上可以理解“服务器推”了,这个词叫的非常形象,服务器主动的发出内容,而不需要客户端的请求。
但是对于我们B/S结构,浏览器不发出个请求服务器是不会知道它的存在的,这样又会陷入到一个漩涡中,既然这样我们如何利用“服务器推”呢?
看下题目,我们的解决方法就是建立一个Http长连接。Http长连接有点C/S结构的意思,类似你玩的网游,这种结构的东西都是长时间连接的。有多种方法可以实现,下面我简单介绍两种。
基于 AJAX的长轮询(long-polling)方式
如上图所示,AJAX 的出现使得 JavaScript可以调用 XMLHttpRequest对象发出 HTTP请求,JavaScript响应处理函数根据服务器返回的信息对 HTML页面的显示进行更新。使用 AJAX实现“服务器推”与传统的 AJAX应用不同之处在于:
1.服务器端会阻塞请求直到有数据传递或超时才返回。
2.客户端 JavaScript响应处理函数会在处理完服务器返回的信息后,再次发出请求,重新建立连接。
3.当客户端处理接收的数据、重新建立连接时,服务器端可能有新的数据到达;这些信息会被服务器端保存直到客户端重新建立连接,客户端会一次把当前服务器端所有的信息取回。
一些应用及示例如 “Meebo”, “Pushlet Chat”都采用了这种长轮询的方式。相对于“轮询”(poll),这种长轮询方式也可以称为“拉”(pull)。因为这种方案基于 AJAX,具有以下一些优点:请求异步发出;无须安装插件;IE、Mozilla FireFox都支持 AJAX。
在这种长轮询方式下,客户端是在 XMLHttpRequest的 readystate为 4(即数据传输结束)时调用回调函数,进行信息处理。当 readystate为 4时,数据传输结束,连接已经关闭。Mozilla Firefox提供了对 Streaming AJAX的支持,即 readystate为 3时(数据仍在传输中),客户端可以读取数据,从而无须关闭连接,就能读取处理服务器端返回的信息。IE在 readystate为 3时,不能读取服务器返回的数据,目前 IE不支持基于 Streaming AJAX。
基于 Iframe及 htmlfile的流(streaming)方式
iframe 是很早就存在的一种 HTML标记,通过在 HTML页面里嵌入一个隐蔵帧,然后将这个隐蔵帧的 SRC属性设为对一个长连接的请求,服务器端就能源源不断地往客户端输入数据。
上节提到的 AJAX 方案是在 JavaScript里处理 XMLHttpRequest从服务器取回的数据,然后 Javascript可以很方便的去控制 HTML页面的显示。同样的思路用在 iframe方案的客户端,iframe服务器端并不返回直接显示在页面的数据,而是返回对客户端 Javascript函数的调用,如“<script type="text/javascript">js_func(“data from server ”)</script>”。服务器端将返回的数据作为客户端 JavaScript函数的参数传递;客户端浏览器的 Javascript引擎在收到服务器返回的 JavaScript调用时就会去执行代码。
从图中可以看到,每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接,服务器端可以设置一个超时时间,超时后通知客户端重新建立连接,并关闭原来的连接)。
使用 iframe 请求一个长连接有一个很明显的不足之处:IE、Morzilla Firefox下端的进度栏都会显示加载没有完成,而且 IE上方的图标会不停的转动,表示加载正在进行。Google的天才们使用一个称为“htmlfile”的 ActiveX解决了在 IE中的加载显示问题,并将这种方法用到了 gmail+gtalk产品中。Alex Russell在 “What else is burried down in the depth's of Google's amazing JavaScript?”文章中介绍了这种方法。Zeitoun网站提供的 comet-iframe.tar.gz,封装了一个基于 iframe和 htmlfile的 JavaScript comet对象,支持 IE、Mozilla Firefox浏览器,可以作为参考。
上面两种方法你看不懂也没关系,现在已经有了大量的框架,但是基本都是基于Java的,这个你更大可不必担心,请继续关注我的blog以后的文章。
下面是在做Comet应用的时候需要注意的几点:
不要在同一客户端同时使用超过两个的 HTTP长连接
我们使用 IE 下载文件时会有这样的体验,从同一个 Web服务器下载文件,最多只能有两个文件同时被下载。第三个文件的下载会被阻塞,直到前面下载的文件下载完毕。这是因为 HTTP 1.1规范中规定,客户端不应该与服务器端建立超过两个的 HTTP连接,新的连接会被阻塞。而 IE在实现中严格遵守了这种规定。
HTTP 1.1 对两个长连接的限制,会对使用了长连接的 Web应用带来如下现象:在客户端如果打开超过两个的 IE窗口去访问同一个使用了长连接的 Web服务器,第三个 IE窗口的 HTTP请求被前两个窗口的长连接阻塞。
所以在开发长连接的应用时,必须注意在使用了多个 frame的页面中,不要为每个 frame的页面都建立一个 HTTP长连接,这样会阻塞其它的 HTTP请求,在设计上考虑让多个 frame的更新共用一个长连接。
服务器端的性能和可扩展性
一般 Web 服务器会为每个连接创建一个线程,如果在大型的商业应用中使用 Comet,服务器端需要维护大量并发的长连接。在这种应用背景下,服务器端需要考虑负载均衡和集群技术;或是在服务器端为长连接作一些改进。
应用和技术的发展总是带来新的需求,从而推动新技术的发展。HTTP 1.1与 1.0规范有一个很大的不同:1.0规范下服务器在处理完每个 Get/Post请求后会关闭套接口连接;而 1.1规范下服务器会保持这个连接,在处理两个请求的间隔时间里,这个连接处于空闲状态。 Java 1.4引入了支持异步 IO的 java.nio包。当连接处于空闲时,为这个连接分配的线程资源会返还到线程池,可以供新的连接使用;当原来处于空闲的连接的客户发出新的请求,会从线程池里分配一个线程资源处理这个请求。这种技术在连接处于空闲的机率较高、并发连接数目很多的场景下对于降低服务器的资源负载非常有效。
但是 AJAX 的应用使请求的出现变得频繁,而 Comet则会长时间占用一个连接,上述的服务器模型在新的应用背景下会变得非常低效,线程池里有限的线程数甚至可能会阻塞新的连接。Jetty 6 Web服务器针对 AJAX、Comet应用的特点进行了很多创新的改进,请参考文章“AJAX,Comet and Jetty”
控制信息与数据信息使用不同的 HTTP连接
使用长连接时,存在一个很常见的场景:客户端网页需要关闭,而服务器端还处在读取数据的堵塞状态,客户端需要及时通知服务器端关闭数据连接。服务器在收到关闭请求后首先要从读取数据的阻塞状态唤醒,然后释放为这个客户端分配的资源,再关闭连接。
所以在设计上,我们需要使客户端的控制请求和数据请求使用不同的 HTTP连接,才能使控制请求不会被阻塞。
在实现上,如果是基于 iframe流方式的长连接,客户端页面需要使用两个 iframe,一个是控制帧,用于往服务器端发送控制请求,控制请求能很快收到响应,不会被堵塞;一个是显示帧,用于往服务器端发送长连接请求。如果是基于 AJAX的长轮询方式,客户端可以异步地发出一个 XMLHttpRequest请求,通知服务器端关闭数据连接。
在客户和服务器之间保持“心跳”信息
在浏览器与服务器之间维持一个长连接会为通信带来一些不确定性:因为数据传输是随机的,客户端不知道何时服务器才有数据传送。服务器端需要确保当客户端不再工作时,释放为这个客户端分配的资源,防止内存泄漏。因此需要一种机制使双方知道大家都在正常运行。在实现上:
1.服务器端在阻塞读时会设置一个时限,超时后阻塞读调用会返回,同时发给客户端没有新数据到达的心跳信息。此时如果客户端已经关闭,服务器往通道写数据会出现异常,服务器端就会及时释放为这个客户端分配的资源。
2.如果客户端使用的是基于 AJAX的长轮询方式;服务器端返回数据、关闭连接后,经过某个时限没有收到客户端的再次请求,会认为客户端不能正常工作,会释放为这个客户端分配、维护的资源。
3.当服务器处理信息出现异常情况,需要发送错误信息通知客户端,同时释放资源、关闭连接。
参考资料:
developerWorks 文章“面向 Java 开发人员的 Ajax: 使用 Jetty 和 Direct Web Remoting 编写可扩展的 Comet 应用程序”:受异步服务器端事件驱动的 Ajax应用程序实现较为困难,本文介绍了一种结合使用 Comet模式和 Jetty 6 Continuations API的解决方法。“Comet: Low Latency Data for the Browser”:Alex Russell是 Dojo Toolkit的项目主管和 Dojo Foundation的主席,他在这篇博客文章中提出了Comet
这个术语。“What else is burried down in the depth’s of Google’s amazing JavaScript?”(Alex Russel,2006年 2月):Alex在这篇文章里介绍了如何使用“htmlfile”ActiveX控件解决 iframe请求长连接时 IE的加载显示问题。Comet wiki:提供了很多开源 Comet框架的链接。Jetty:Jetty是一种开源的基于标准的 Web服务器,完全使用 Java语言实现。“Ajax, Comet and Jetty”(Greg Wilkins,Webtide,2006年 1月):Wilkins的这份白皮书讨论了扩展 Ajax连接的 Jetty架构方法。Continuations:了解更多关于 Jetty 的 Continuations特性的信息。“pushlet”:开源 comet 框架,使用了观察者模型。浏览器端提供了基于 AJAX和 iframe的 JavaScript库,服务器端使用 Java Servlet。“How to implement COMET with PHP”:提供的 comet-iframe.tar.gz使用 iframe/htmlfile封装了一个 JavaScript comet对象,支持 IE、Mozilla Firefox浏览器。“AFLAX”:Asynchronous Flash and XML,提供了强大的 Flash、Javascript库和很多范例。developerWorks Ajax技术资源中心:能找到更多关于 Ajax技术的文章和教程。developerWorks Web开发技术专区:提供了关于 Web开发和架构方面的大量文章。developerWorks Java技术专区:提供了关于 Java编程各个方面的数百篇文章
上面的参考资料欢迎大家学习。
下面是例子
上面说了实现方法,这个例子就是来源于上面的第二种方法——一个隐藏的iframe。
作者写的代码都非常详细了,大家都能看懂。但是有个严重的问题,就是服务器会自动断开链接,我下面详细说。
原理:利用htmlfile这个ActiveX,往页面上放一个iframe,设置它的src为请求的地址。
但是这个代码有问题,就是服务器会自动断开链接。我不知道是作者代码的问题??(我看不出哪里有问题),还是浏览器或者PHP的问题。
但是有解决方法,就是设置一个数字,当请求到达一定次数后,就重新让客户端发送一次请求,重新建立一个长链接。这个方法能暂时解决但是不是最好的方法。而且我上面说的那个次数你得自己估摸着,差不多长连接快短的时候就建立,如果你这个次数设置的短了费资源,设置的长了连接已经断了很长时间你再链接就跟AJAX没什么分别了。
但是还有问题,就算是用上面的方法,我不知道是我机器的问题还是怎么回事,在FF下可以长时间连接,我测试了有半个小时没问题,但是在IE7下还是会断。
本来想的是写一个JQuery的Comet插件,但是现在想想意义不大,因为PHP玩Comet纯粹属于没事找抽型。与现有的那些JavaComet框架比起来真实相形见绌。
所以大家也只是看下例子,学习下代码,不要当真了。
代码我已经修改好了。大家看下效果就行了,当它是个花瓶吧。
很遗憾。我知道很多人看完这么多字的教程后看到作者说这个技术是个花瓶,不学为罢。我也深表歉意,为了大家不走弯路,如果以后想要开发实时的聊天室这一类的东西,我劝你不要用PHP,不是PHP不行,而是你走没人走过的路是比较艰难的。劝你还是用Java,因为有很多Comet for Java的框架了,或者直接用flash算了。