预备知识
web的工作原理,可以概括为:
- 客户端(浏览器)通过TCP/IP协议,建立到服务器的TCP链接。
- 客户端向服务器发送HTTP协议请求包,请求服务器里的资源文档,
- 服务器向客户端发送HTTP协议应答包。
- 客户端与服务器断开,客户端解释HTML文档,在客户端屏幕上渲染返回结果。
那么,可以发现,HTTP协议有两个问题:
- HTTP是无状态协议,所以,上次的请求和这次的请求之间没有关系。
- HTTP协议是单向的,也就是说,服务器不能向客户端发送请求。
- 对于第一个问题,可以在服务器使用session,或者在客户端使用cookie,能够解决“无状态协”议这个问题.
- 对于第二个问题,就是此篇博客想要讨论。
本文将比较三种服务器推送方式。
- ajax轮询
- longpoll
- websocket
在实例代码中,我前台使用javascript和一些jquery,后台web服务器使用了tornado。之所以使用tornado,是因为
- 人生苦短,我用python :-)
- tornado对long poll,和web socket都有很好的支持,而且,tornado是异步非阻塞式服务器,处理起需要服务器推送,交互性强的web应用,非常适合。
我的代码要实现这样一个例子:
在服务器端,保存一个字符串,连接到服务器的用户,将看到这个字符串,同时也可以修改它,当修改此字符串的时候,连接到此服务器的其他用户,能够在不主动刷新的情况下,看到修改。这就用到了“服务器推”的技术
1.ajax轮询
所谓ajax轮询实现服务器推技术,就是客户端,每隔一段时间,“偷偷”的发送ajax请求,之后,将返回的数据局部刷新,这样感觉就像是服务器在推数据.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51
| import tornado.web import tornado.httpserver import tornado.ioloop import tornado.options
import json
class Announce(): subject = "nima" def getSubjson(self): return json.dumps({'content':self.subject})
class MainHandler(tornado.web.RequestHandler): def get(self): self.render("index.html")
class ChatHandler(tornado.web.RequestHandler): def post(self): self.application.announce.subject = self.get_argument('content') self.write(self.application.announce.getSubjson()) def get(self): self.write(self.application.announce.getSubjson()) class Application(tornado.web.Application): """ """
def __init__(self): """ """ self.announce = Announce() handlers = [ (r'/',MainHandler), (r'/chat',ChatHandler), ] settings = { 'template_path': 'templates', 'static_path': 'static', 'debug': True } tornado.web.Application.__init__(self, handlers, **settings)
if __name__ == '__main__': tornado.options.parse_command_line() app = Application() server = tornado.httpserver.HTTPServer(app) server.listen(8000) tornado.ioloop.IOLoop.instance().start()
|
下面是javascript代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| <script type="text/javascript"> $(function(){ updateMsg();
$("#mypp").click(function(){ $.post("chat",{ content: $("#message").val() },function(data, textStatus){ var content = data.content; var txt = "<p>"+content+"</p>"; $("#chatContent").html(txt); },"json"); }); });
function updateMsg(){ $.get("chat",{},function(data, textStatus){ var content = data.content var txt = "<p>"+content+"</p>"; $("#chatContent").html(txt); },"json"); setTimeout(updateMsg, 4000); } </script>
|
updateMsg 隔一段时间,会发送数据请求,注意setTimeout(updateMsg, 4000);定时器
可以看出,ajax轮询有两个明显的缺点,
- 如果轮询的频率低服务器端的数据更新后,客户端不能马上更新
- 如果轮询的频率高,那么服务器大多是些无用的请求,反而会增加服务器的压力。
Long Poll
那么就来看看第二种方式,long poll, 这算是一种”真正的”的服务器推送技术(comet)
long poll的原理是,客户端与服务器将建立一条长连接,也就是说,客户端会发出一个请求,而服务器,将阻塞请求,直到有数据需要传递,才会返回。
返回之后,客户端将关闭此连接,然后再次发出一个请求,建立一个新的连接,再次等待服务器推送数据.
服务器端实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
| import tornado.web import tornado.httpserver import tornado.ioloop import tornado.options
from uuid import uuid4 import json
class Announce(): subject = "nima" callbacks = []
def register(self, callback): self.callbacks.append(callback)
def changeSubject(self, data): self.subject = data self.notifyCallbacks()
def getJson(self): return json.dumps({'content':self.subject})
def notifyCallbacks(self): for c in self.callbacks: self.callbackHelper(c)
self.callbacks = []
def callbackHelper(self, callback): callback(self.getJson())
class MainHandler(tornado.web.RequestHandler): def get(self): self.render("index.html")
class ChatHandler(tornado.web.RequestHandler): def post(self): content = self.get_argument('content') self.application.announce.changeSubject(content)
class StatusHandler(tornado.web.RequestHandler): @tornado.web.asynchronous def get(self): self.application.announce.register(self.async_callback(self.on_message))
def on_message(self, data): self.write(data) self.finish()
class Application(tornado.web.Application): """ """
def __init__(self): """ """ self.announce = Announce() handlers = [ (r'/',MainHandler), (r'/chat',ChatHandler), (r'/status',StatusHandler), ] settings = { 'template_path': 'templates', 'static_path': 'static', 'debug': True } tornado.web.Application.__init__(self, handlers, **settings)
if __name__ == '__main__': tornado.options.parse_command_line() app = Application() server = tornado.httpserver.HTTPServer(app) server.listen(8000) tornado.ioloop.IOLoop.instance().start()
|
客户端与服务器端的链接,会一直保存着,当发生改变时,服务器才会把数据推送给客户端.
这其实就是设计模式中的观察者模式。
客户端js代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <script type="text/javascript"> $(function(){
setTimeout(requestInventory, 100);
$("#mypp").click(function(){ $.post("//localhost:8000/chat",{ content: $("#message").val() },type="json"); }); });
function requestInventory(){ $.getJSON("//localhost:8000/status",{},function(data, Status, xhr){ var content = data.content; var txt = "<p>"+content+"</p>" $("#chatContent").html(txt); setTimeout(requestInventory, 0); }); } </script>
|
可以看到,在requestInventory()中,每次数据返回后,setTimeout(requestInventory, 0);将建立一条新的链接.
其实还有一种类似与long poll的技术,iframe流方式,这种方式在页面插入一个隐藏的iframe.利用其src属性,在服务器和客户端之间建立一条长连接。与long poll不同的是,iframe流的这条连接会一直存在,而不是像long poll在数据返回后,客户端关闭此连接,然后重新开启一条连接。
但是,comet中采用的长连接,也会大量的消耗服务器的带宽和资源。
Websocket
websocket,是html5引入的一个特性,也是未来的趋势,web socket 是在浏览器和服务器之间进行全双工通信的网络技术,既然是全双工通信,那么服务器自然可以主动传送数据给服务器,而且通信协议的header也很小,相比与之前的long poll, web socket 能够更好的节省服务器资源和宽带并达到实时通信.
服务器端代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83
| import tornado.web import tornado.websocket import tornado.httpserver import tornado.ioloop import tornado.options
import json
class Announce(): subject = "nima" callbacks = []
def register(self, callback): self.callbacks.append(callback)
def unregister(self, callback): self.callbacks.remove(callback)
def getJson(self): return json.dumps({'content':self.subject})
def changeSubject(self, data): self.subject = data self.notifyCallbacks()
def notifyCallbacks(self): for c in self.callbacks: self.callbackHelper(c)
def callbackHelper(self, callback): callback(self.getJson())
class MainHandler(tornado.web.RequestHandler): def get(self): self.render("index.html")
class ChatHandler(tornado.web.RequestHandler): def post(self): content = self.get_argument('content') self.application.announce.changeSubject(content)
class StatusHandler(tornado.websocket.WebSocketHandler): def open(self): self.application.announce.register(self.callback)
def on_close(self): self.application.announce.unregister(self.callback)
def on_message(self, message): pass
def callback(self, data): self.write_message(data)
class Application(tornado.web.Application): """ """
def __init__(self): """ """ self.announce = Announce() handlers = [ (r'/',MainHandler), (r'/chat',ChatHandler), (r'/status',StatusHandler), ] settings = { 'template_path': 'templates', 'static_path': 'static', 'debug': True } tornado.web.Application.__init__(self, handlers, **settings)
if __name__ == '__main__': tornado.options.parse_command_line() app = Application() server = tornado.httpserver.HTTPServer(app) server.listen(8000) tornado.ioloop.IOLoop.instance().start()
|
客户端,也是js对websocket 的操作.
注意 var host =’ws://localhost:8000/status’
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| <script type="text/javascript"> $(function(){ requestInventory(); $("#mypp").click(function(){ $.post("chat",{ content: $("#message").val() },type="json"); }); });
function requestInventory(){ var host ='ws://localhost:8000/status' var websocket = new WebSocket(host);
websocket.onopen = function (evt) {}; websocket.onmessage = function (evt){ var content = $.parseJSON(evt.data)['content']; var txt = "<p>"+ content +"</p>" $("#chatContent").html(txt); }; websocket.onerror = function (evt) {}; } </script>
|
html5就是未来的趋势,而且,chrome,firefox,opera和safari都支持,IE从版本10开始也支持了。