客户端与服务器持续同步解析(轮询,comet,WebSocket)在B/S模型的Web应用中,客户端常常需要保持和服务器的持续更新。这种对及时性要求比较高的应用比如:股票价格的查询,实时

客户端与服务器持续同步解析(轮询,comet,WebSocket)在B/S模型的Web应用中,客户端常常需要保持和服务器的持续更新。这种对及时性要求比较高的应用比如:股票价格的查询,实时 普通的轮询Polling)Comet基于服务器长连接的“服务器推”技术。这其中又分为两种基于AJAX和基于IFrame的流streaming方式。基于AJAX的长轮询long-polling方式。WebSocket古老的轮询轮询最简单也最容易实现每隔一段时间向服务器发送查询有更新再触发相关事件。对于前端使用js的setInterval以AJAX或者JSONP的方式定期向服务器发送request。var polling function(){ $.post(/polling, function(data, textStatus){ $(p).append(databr); }); }; 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即readystate3时数据仍在传输客户端可以读取数据而不用关闭连接。问题也在这里IE 在 readystate 为 3 时不能读取服务器返回的数据所以目前 IE 不支持基于 Streaming AJAX而长轮询由于是普通的AJAX请求所以没有浏览器兼容问题。另外由于使用streaming方式控制权在服务器端并且在长连接期间并没有客户端到服务器端的数据所以不能根据客户端的数据进行即时的适应比如检查cookie等等而对于long polling方式在每次断开连接之后可以进行判断。所以综合来说long polling是现在比较主流的做法如fbPlurk。接下来我们就来对流streaming和长轮询long-polling两种方式进行演示。流streaming方式从上图可以看出每次数据传送不会关闭连接连接只会在通信出现错误时或是连接重建时关闭一些防火墙常被设置为丢弃过长的连接 服务器端可以设置一个超时时间 超时后通知客户端重新建立连接并关闭原来的连接。流方式首先一种常用的做法是使用AJAX的流方式如先前所说此方法主要判断readystate3时的情况所以不能适用于IE。服务器端代码像这样class StreamingHandler(tornado.web.RequestHandler): 使用asynchronus装饰器使得post方法变成无阻塞 tornado.web.asynchronous def post(self): self.get_data(callbackself.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(callbackself.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 doesnt support window.XMLHttpRequest); } var pos 0; request.onreadystatechange function () { if (request.readyState 3) { //在 Interactive 模式处理 var data request.responseText; $(p).append(data.substring(pos)br); 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(callbackself.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(scriptparent.add_content(Server says: %dbr /);/script % data) # 输出的立刻执行调用父窗口js函数add_content self.flush() tornado.ioloop.IOLoop.instance().add_timeout( time.time()3, lambda: self.get_data(callbackself.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(callbackself.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(databr); } catch(e){ updater.onError(); return; } interval window.setTimeout(updater.poll, 0); }, onError: function(){ console.log(Poll error;); } };