Browser和Server持续同步的几种方式(jQuery+tornado演示)
在B/S模型的Web应用中,客户端常常需要保持和服务器的持续更新。这种对及时性要求比较高的应用比如:股票价格的查询,实时的商品价格,自动更新的twitter timeline以及基于浏览器的聊天系统(如GTalk)等等。由于近些年AJAX技术的兴起,也出现了多种实现方式。本文将对这几种方式进行说明,并用jQuery+tornado进行演示,需要说明的是,如果对tornado不了解也没有任何问题,由于tornado的代码非常清晰且易懂,选择tornado是因为其是一个非阻塞的(Non-blocking IO)异步框架(本文使用2.0版本)。
在开始之前,为了让大家有个清晰的认识,首先列出本文所要讲到的内容大概。本文将会分以下几部分:
- 普通的轮询(Polling)
- Comet:基于服务器长连接的“服务器推”技术。这其中又分为两种:
- 基于AJAX和基于IFrame的流(streaming)方式。
- 基于AJAX的长轮询(long-polling)方式。
- WebSocket
古老的轮询
轮询最简单也最容易实现,每隔一段时间向服务器发送查询,有更新再触发相关事件。对于前端,使用js的setInterval以AJAX或者JSONP的方式定期向服务器发送request。
var polling = function(){ $.post('/polling', function(data, textStatus){ $("p").append(data+"
"); }); }; interval = setInterval(polling, 1000);
后端我们只是象征性地随机生成一些数字,并且返回。在实际应用中可以访问cache或者从数据库中获取内容。
import random import tornado.web class PollingHandler(tornado.web.RequestHandler): def post(self): num = random.randint(1, 100) self.write(str(num))
可以看到,采用polling的方式,效率是十分低下的,一方面,服务器端不是总有数据更新,所以每次问询不一定都有更新,效率低下;另一方面,当发起请求的客户端数量增加,服务器端的接受的请求数量会大量上升,无形中就增加了服务器的压力。
Comet:基于HTTP长连接的“服务器推”技术
看到 这个标题有的人可能就晕了,其实原理还是比较简单的。基于Comet的技术主要分为流(streaming)方式和长轮询(long-polling)方式。
首先看Comet这个单词,很多地方都会说到,它是“彗星”的意思,顾名思义,彗星有个长长的尾巴,以此来说明客户端发起的请求是长连的。即用户发起请求后就挂起,等待服务器返回数据,在此期间不会断开连接。流方式和长轮询方式的区别就是:对于流方式,客户端发起连接就不会断开连接,而是由服务器端进行控制。当服务器端有更新时,刷新数据,客户端进行更新;而对于长轮询,当服务器端有更新返回,客户端先断开连接,进行处理,然后重新发起连接。
会有同学问,为什么需要流(streaming)和长轮询(long-polling)两种方式呢?是因为:对于流方式,有诸多限制。如果使用AJAX方式,需要判断XMLHttpRequest 的 readystate,即readystate==3时(数据仍在传输),客户端可以读取数据,而不用关闭连接。问题也在这里,IE 在 readystate 为 3 时,不能读取服务器返回的数据,所以目前 IE 不支持基于 Streaming AJAX,而长轮询由于是普通的AJAX请求,所以没有浏览器兼容问题。另外,由于使用streaming方式,控制权在服务器端,并且在长连接期间,并没有客户端到服务器端的数据,所以不能根据客户端的数据进行即时的适应(比如检查cookie等等),而对于long polling方式,在每次断开连接之后可以进行判断。所以综合来说,long polling是现在比较主流的做法(如fb,Plurk)。
接下来,我们就来对流(streaming)和长轮询(long-polling)两种方式进行演示。
流(streaming)方式
从上图可以看出每次数据传送不会关闭连接,连接只会在通信出现错误时,或是连接重建时关闭(一些防火墙常被设置为丢弃过长的连接, 服务器端可以设置一个超时时间, 超时后通知客户端重新建立连接,并关闭原来的连接)。
流方式首先一种常用的做法是使用AJAX的流方式(如先前所说,此方法主要判断readystate==3时的情况,所以不能适用于IE)。
服务器端代码像这样:
class StreamingHandler(tornado.web.RequestHandler): '''使用asynchronus装饰器使得post方法变成无阻塞''' @tornado.web.asynchronous def post(self): self.get_data(callback=self.on_finish) def get_data(self, callback): if self.request.connection.stream.closed(): return num = random.randint(1, 100) #生成随机数 callback(num) #调用回调函数 def on_finish(self, data): self.write("Server says: %d" % data) self.flush() tornado.ioloop.IOLoop.instance().add_timeout( time.time()+3, lambda: self.get_data(callback=self.on_finish) )
对于服务器端,仍然是生成随机数字,由于要不断输出数据,于是在回调函数里延迟3秒,然后继续调用get_data方法。在这里要注意的是,不能使用time.sleep(),由于tornado是单线程的,使用sleep方法会block主线程。因此要调用IOLoop的add_timeout方法(参数0:执行时间戳,参数1:回调函数)。于是服务器端会生成一个随机数字,延迟3秒再生成随机数字,循环往复。
于是前端js就是:
try { var request = new XMLHttpRequest(); } catch (e) { alert("Browser doesn't support window.XMLHttpRequest"); } var pos = 0; request.onreadystatechange = function () { if (request.readyState === 3) { //在 Interactive 模式处理 var data = request.responseText; $("p").append(data.substring(pos)+"
"); pos = data.length; } }; request.open("POST", "/streaming", true); request.send(null);
对于tornado来说,调用flush方法,会将先前write的所有数据都发送客户端,也就是response的数据处于累加的状态,所以在js脚本里,我们使用了pos变量作为cursor来存放每次flush数据结束位置。
另外一种常用方法是使用IFrame的streaming方式,这也是早先的常用做法。首先我们在页面里放置一个iframe,它的src设置为一个长连接的请求地址。Server端的代码基本一致,只是输出的格式改为HTML,用来输出一行行的Inline Javascript。由于输出就得到执行,因此就少了存储游标(pos)的过程。服务器端代码像这样:
class IframeStreamingHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def get(self): self.get_data(callback=self.on_finish) def get_data(self, callback): if self.request.connection.stream.closed(): return num = random.randint(1, 100) callback(num) def on_finish(self, data): self.write("<script>parent.add_content('Server says: %d<br />');</script>" % data) # 输出的立刻执行,调用父窗口js函数add_content self.flush() tornado.ioloop.IOLoop.instance().add_timeout( time.time()+3, lambda: self.get_data(callback=self.on_finish) )
在客户端我们只需定义add_content函数:
var add_content = function(str){ $("p").append(str); };
由此可以看出,采用IFrame的streaming方式解决了浏览器兼容问题。但是由于传统的Web服务器每次连接都会占用一个连接线程,这样随着增加的客户端长连接到服务器时,线程池里的线程最终也就会用光。因此,Comet长连接只有对于非阻塞异步Web服务器才会产生作用。这也是为什么选择tornado的原因。
使用iframe方式一个问题就是浏览器会一直处于加载状态。
长轮询(long-polling)方式
长轮询是现在最为常用的方式,和流方式的区别就是服务器端在接到请求后挂起,有更新时返回连接即断掉,然后客户端再发起新的连接。于是Server端代码就简单好多,和上面的任务类似:
class LongPollingHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def post(self): self.get_data(callback=self.on_finish) def get_data(self, callback): if self.request.connection.stream.closed(): return num = random.randint(1, 100) tornado.ioloop.IOLoop.instance().add_timeout( time.time()+3, lambda: callback(num) ) # 间隔3秒调用回调函数 def on_finish(self, data): self.write("Server says: %d" % data) self.finish() # 使用finish方法断开连接
Browser方面,我们封装成一个updater对象:
var updater = { poll: function(){ $.ajax({url: "/longpolling", type: "POST", dataType: "text", success: updater.onSuccess, error: updater.onError}); }, onSuccess: function(data, dataStatus){ try{ $("p").append(data+"
"); } catch(e){ updater.onError(); return; } interval = window.setTimeout(updater.poll, 0); }, onError: function(){ console.log("Poll error;"); } };
要启动长轮询只要调用
updater.poll();
可以看到,长轮询与普通的轮询相比更有效率(只有数据更新时才返回数据),减少不必要的带宽的浪费;同时,长轮询又改进了streaming方式对于browser端判断并更新不足的问题。
WebSocket:未来方向
以上不管是Comet的何种方式,其实都只是单向通信,直到WebSocket的出现,才是B/S之间真正的全双工通信。不过目前WebSocket协议仍在开发中,目前Chrome和Safri浏览器默认支持WebSocket,而FF4和Opera出于安全考虑,默认关闭了WebSocket,IE则不支持(包括9),目前WebSocket协议最新的为“76号草案”。有兴趣可以关注http://dev.w3.org/html5/websockets/。
在每次WebSocket发起后,B/S端进行握手,然后就可以实现通信,和socket通信原理是一样的。目前,tornado2.0版本也是实现了websocket的“76号草案”。详细可以参阅文档。我们还是只是在通信打开之后发送一堆随机数字,仅演示之用。
import tornado.websocket class WebSocketHandler(tornado.websocket.WebSocketHandler): def open(self): for i in xrange(10): num = random.randint(1, 100) self.write_message(str(num)) def on_message(self, message): logging.info("getting message %s", message) self.write_message("You say:" + message)
客户端代码也很简单和直观:
var wsUpdater = { socket: null, start: function(){ if ("WebSocket" in window) { wsUpdater.socket = new WebSocket("ws://localhost:8889/websocket"); } else { wsUpdater.socket = new MozWebSocket("ws://localhost:8889/websocket"); } wsUpdater.socket.onmessage = function(event) { $("p").append(event.data+"
"); }; } }; wsUpdater.start();
总结:本文对Browser和Server端持续同步的方式进行了介绍,并进行了演示。在实际生产中,有一些框架。包括Java的Pushlet,NodeJS的socket.io,大家请自行查阅资料。
本文参考文章:
客户端使用socket.io可以自动选择,使用什么方式。如果是chrome,使用websocket.github上有基于tornado修改的https://github.com/SocketTornadIO/SocketTornad.IO
socket.io来做持续同步比jquery靠谱。
socket.io本身就是定位于b/s间通信,jQuery本身只是js框架,没什么靠谱不靠谱之说。我用jQuery也就是演示作用。
SocketTornadIO似乎是个不错的项目,感谢你的提醒。具体生产环境还是看自己的选择吧。
很棒的文章:)
呵呵:)还有很多要学习的,多交流~
看了两篇文章.一篇是写编辑距离的.一篇就是这个的.
懂NLP又懂JS代码的人不多.
正是因为每一个Comet的长连接需要占用一个连接线程.所以后端的Server使用Erlang更合适一些.
希望有时间能多交流.
Erlang等并行语言就是对于并发的处理能力比较优秀(应用比如说rabbitmq)。不过对于http连接通常说异步非阻塞即可。
秦兄把Yui+PHP的代码改成了jQuery+Python,看起来舒服多了,PHP代码看上去一坨一坨的
呃.
这个不好说.
一般如果维持这种长连接只是用做消息传递.不会有太多的业务逻辑部分.
所以感觉Erlang用做这里更合适.
如果用带有业务逻辑的做异步非阻塞的Web服务器,似乎在功能上就把消息传递和业务处理耦合在一起了.
Ajax长轮询有个问题是,如果在pending阶段,那么会阻塞其它的ajax.
这个如何解决呢?
我刚刚做了实验,在long-polling pending的过程中,并没有block另一个ajax请求亚
写的太好了,清晰易懂,忍不住留个言呵呵。
学习了,谢谢。
我也是交大软院的,我在嵌入式实验室,你的导师是谁呢?
我的导师是吴刚,你是今年新生么?
能不能发给我一个
完整的 流(streaming)方式 的python server脚本和html啊
弄了半天出不来
代码差不多都在里面了丫,当时写的测试代码已经找不到了...
能加qq指导一下吗?
抱歉,刚看到...最近到期末了,考试季...悬啊...
正在弄这方面的东东, 忍不住留言。 作者写得实在太好了, 有空多交流
呵呵,多谢:)有空多交流
好文,支持一下。
lz 这篇文章写得不错。
清晰明了。
哇靠 ,这是楼主原创吗 ???
在另一个人博客上有同样一篇文章
http://www.cnblogs.com/pannysp/archive/2012/02/10/2346084.html
如是原创,那楼主你被盗版了,如果不是,楼主得注明原文出处吧
多亏了你提醒,这个道德沦丧的时代啊:'(
别的不敢保证,我的博客里的文章都是原创,如果有参考别人的部分,我也会在文章里说明的。
并且,这篇文章11年8月就写了,copy的那个是12年2月的。
好厉害!
这里也见到你 嘿嘿
总结的蛮全面的.谢谢
不错不错,之前为了过个在线聊天室,抓了半天的webqq拉取消息的请求。。。
看了你这个之后明白多了。
建议在实际应用中将 “请求超时”考虑进去
如果是c/s这种形式呢?
C/S那就tcp或者udp连接啊
我的问题大概是这样的:
1、客户端程序将当前位置发送给服务器;
2、服务器端记录进入该区域的用户及状态,并将该用户最新状态返回到客户端(pc)
3、程序一直运行
我现在需要在客户端连接server,并展示Server返回的数据。
不是很清楚要怎么实现
文章写得很清析,谢谢
给作者留言
关于作者
残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。
博客分类
搜索
点击排行
标签云
扫描访问
主题
残阳似血的微博
登录
最新评论
admin22 在文章“Django mptt介绍以及使用”下评论
111111
admin 在文章“Django mptt介绍以及使用”下评论
11221
admin 在文章“Django mptt介绍以及使用”下评论
sdsaa
admin 在文章“Django mptt介绍以及使用”下评论
sdfsd
iceworld 在文章“Neo4j简介”下评论
请问,这篇文章的代码,使用了什么 maven 依赖?
关于作者
残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。
所有分类
友情链接
联系信息
地址
上海闵行区东川路800号
上海交通大学
200240
Mail
chinekingseu@gmail.com
QQ
344861256