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)等。
博客分类
搜索
点击排行
标签云
扫描访问
主题
残阳似血的微博
登录
最新评论
tofu 在文章“在数据库中存储层级结构”下评论
还有一种闭包表可以讲讲
yosong 在文章“Django mptt介绍以及使用”下评论
。
yosong 在文章“Django mptt介绍以及使用”下评论
这个多级评论咋做呀,啊啊啊啊啊啊
热搜 在文章“PyOdps 0.4版本发布,从一个故事说起”下评论
文章不错交个朋友
啊啊 在文章“Django mptt介绍以及使用”下评论
console.log('helloworld')
关于作者
残阳似血(@秦续业),程序猿一枚,把梦想揣进口袋的挨踢工作者。现加入阿里云,研究僧毕业于上海交通大学软件学院ADC实验室。熟悉分布式数据分析(DataFrame并行化框架)、基于图模型的分布式数据库和并行计算、Dpark/Spark以及Python web开发(Django、tornado)等。
所有分类
友情链接
联系信息
地址
上海闵行区东川路800号
上海交通大学
200240
Mail
chinekingseu@gmail.com
QQ
344861256