pyList

服务器推送技术 Tornado comet

long poll的原理是,客户端与服务器将建立一条长连接,也就是说,客户端会发出一个请求,而服务器,将阻塞请求,直到有数据需要传递,才会返回。

返回之后,客户端将关闭此连接,然后再次发出一个请求,建立一个新的连接,再次等待服务器推送数据.

服务器端实现:

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)

# StatusHandler的处理是异步的
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)
        # 必须要finish 否则服务器会一直阻塞
        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代码:

<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 能够更好的节省服务器资源和宽带并达到实时通信.

服务器端代码 :

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)

# 注意,这个类的父类,用来进行web socket的WebSocketHandler
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'

<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开始也支持了。

关于知乎的服务器推送

by 孙竟,知乎工程师

Chrome下使用的是JSONP Long Polling(究竟起什么名字别计较),其他浏览器没去看。

所谓JSONP,就是生成一个script标签,把回调函数的名称作为HTTP请求的参数。服务器解析回调函数名,返回类似callback(data)的代码,使浏览器可以跨域执行异步请求。

至于知乎想跨域执行的原因,估计是想让专门的服务器来处理事件更新,并减少cookie的传输量吧。

而Long Polling是说服务器在接收到这个请求后,阻塞而不立刻返回。如果有新的事件,或者达到超时时间(知乎目前设为30秒),再响应这个请求。

客户端接收到响应后,再发起一个新的请求。

这比轮询节省了无谓的开销,但要在服务器端保持大量长连接(Tornado正好擅长这个)。

想效率更高的话可以使用streaming,也就是在使用HTTP长连接的同时,将Transfer-Encoding设为chunked。当服务器有新的事件时,就发送响应,但不关闭连接。

客户端接收到响应后,处理该响应,也不关闭连接。

服务器想再次发送响应时,直接使用现有的连接即可。

这比Long Polling节省了多次建立和关闭连接的过程,也不用担心重建连接时错过新事件(如果你没在服务器端保存的话)。但因为是单向的,就需要客户端每隔一段时间发送心跳包,以证明客户端还在线。 此外,这种方式明显不能用JSONP实现,因为script标签在全部加载完前是不会执行里面的代码的,因此一次HTTP请求,只有一次执行机会。

还想高效就用WebSocket吧。它仍然以HTTP作为传输层,在建立连接时,客户端发起WebSocket握手请求,其头部内容沿用HTTP协议的定义;服务器接到请求后,返回验证信息,并用101状态码提示切换到WebSocket协议。这样一个WebSocket连接就建立好了。后续发送数据时,就不需要带HTTP请求头了。 它是双向的,因此服务器和客户端都能随时发送数据,且发送完不需要断开连接。任意一方主动断开连接时,对方都会收到onclose事件(服务器端的API就自己实现吧);当然你也能直接用这个连接来发送心跳包,协议里还为此预留了2个操作码(不过暂时没有浏览器端的JavaScript API)。

并且,WebSocket也是能跨域的,所以从功能和性能上来说,它都足够优秀了。

缺点就是低端的浏览器不支持,例如IE 6~IE 9。当然,支持WebSocket的Web服务器也比较少(Tornado表示无压力)。

另外,WebSocket的协议版本很多,各种浏览器支持的版本不一样:

https://en.wikipedia.org/wiki/WebSocket#Browser_support%E3%80%82%E4%BE%8B%E5%A6%82iOS%E8%AE%BE%E5%A4%87%E6%94%AF%E6%8C%81%E7%9A%84hixie-76%E5%B0%B1%E6%98%AF%E5%B7%B2%E8%A2%AB%E6%91%88%E5%BC%83%E7%9A%84%EF%BC%8C%E6%89%80%E4%BB%A5%E5%AE%9E%E7%8E%B0%E6%97%B6%E8%BF%98%E5%BE%97%E8%80%83%E8%99%91%E5%90%91%E4%B8%8B%E5%85%BC%E5%AE%B9%E7%9A%84%E9%97%AE%E9%A2%98%E3%80%82

类似的还能通过Flash 、Silverlight和Java applet等浏览器插件来建立socket连接。缺点是没装插件就用不了,例如iOS设备支持WebSocket,但不支持后三者。

本文网址: https://pylist.com/t/1401931634 (转载注明出处)
如果你有任何建议或疑问可以在下面 留言
Be the first to comment!
Captcha image
Relative Articles