天天看點

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

在B/S模型的Web應用中,用戶端常常需要保持和伺服器的持續更新。這種對及時性要求比較高的應用比如:股票價格的查詢,實時的商品價格,自動更新的twitter timeline以及基于浏覽器的聊天系統(如GTalk)等等。由于近些年AJAX技術的興起,也出現了多種實作方式。本文将對這幾種方式進行說明,并用jQuery+tornado進行示範,需要說明的是,如果對tornado不了解也沒有任何問題,由于tornado的代碼非常清晰且易懂,選擇tornado是因為其是一個非阻塞的(Non-blocking IO)異步架構(本文使用2.0版本)。

在開始之前,為了讓大家有個清晰的認識,首先列出本文所要講到的内容大概。本文将會分以下幾部分:

  1. 普通的輪詢(Polling)
  2. Comet:基于伺服器長連接配接的“伺服器推”技術。這其中又分為兩種:
    1. 基于AJAX和基于IFrame的流(streaming)方式。
    2. 基于AJAX的長輪詢(long-polling)方式。
  3. WebSocket

古老的輪詢

輪詢最簡單也最容易實作,每隔一段時間向伺服器發送查詢,有更新再觸發相關事件。對于前端,使用js的setInterval以AJAX或者JSONP的方式定期向伺服器發送request。

var polling = function(){
    $.post('/polling', function(data, textStatus){
        $("p").append(data+"<br>");
    });
};
interval = setInterval(polling, 1000);      

後端我們隻是象征性地随機生成一些數字,并且傳回。在實際應用中可以通路cache或者從資料庫中擷取内容。

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
import random
import tornado.web

class PollingHandler(tornado.web.RequestHandler):
    def post(self):
        num = random.randint(1, 100)
        self.write(str(num))      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

可以看到,采用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)方式

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

從上圖可以看出每次資料傳送不會關閉連接配接,連接配接隻會在通信出現錯誤時,或是連接配接重建時關閉(一些防火牆常被設定為丢棄過長的連接配接, 伺服器端可以設定一個逾時時間, 逾時後通知用戶端重建立立連接配接,并關閉原來的連接配接)。

流方式首先一種常用的做法是使用AJAX的流方式(如先前所說,此方法主要判斷readystate==3時的情況,是以不能适用于IE)。

伺服器端代碼像這樣:

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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)
        )      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

對于伺服器端,仍然是生成随機數字,由于要不斷輸出資料,于是在回調函數裡延遲3秒,然後繼續調用get_data方法。在這裡要注意的是,不能使用time.sleep(),由于tornado是單線程的,使用sleep方法會block主線程。是以要調用IOLoop的add_timeout方法(參數0:執行時間戳,參數1:回調函數)。于是伺服器端會生成一個随機數字,延遲3秒再生成随機數字,循環往複。

于是前端js就是:

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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)+"<br>");
        pos = data.length;
    }
};
request.open("POST", "/streaming", true);
request.send(null);      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

對于tornado來說,調用flush方法,會将先前write的所有資料都發送用戶端,也就是response的資料處于累加的狀态,是以在js腳本裡,我們使用了pos變量作為cursor來存放每次flush資料結束位置。

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

另外一種常用方法是使用IFrame的streaming方式,這也是早先的常用做法。首先我們在頁面裡放置一個iframe,它的src設定為一個長連接配接的請求位址。Server端的代碼基本一緻,隻是輸出的格式改為HTML,用來輸出一行行的Inline Javascript。由于輸出就得到執行,是以就少了存儲遊标(pos)的過程。伺服器端代碼像這樣:

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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)
        )      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

在用戶端我們隻需定義add_content函數:

var add_content = function(str){
    $("p").append(str);
};      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

由此可以看出,采用IFrame的streaming方式解決了浏覽器相容問題。但是由于傳統的Web伺服器每次連接配接都會占用一個連接配接線程,這樣随着增加的用戶端長連接配接到伺服器時,線程池裡的線程最終也就會用光。是以,Comet長連接配接隻有對于非阻塞異步Web伺服器才會産生作用。這也是為什麼選擇tornado的原因。

使用iframe方式一個問題就是浏覽器會一直處于加載狀态。

長輪詢(long-polling)方式

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

長輪詢是現在最為常用的方式,和流方式的差別就是伺服器端在接到請求後挂起,有更新時傳回連接配接即斷掉,然後用戶端再發起新的連接配接。于是Server端代碼就簡單好多,和上面的任務類似:

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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方法斷開連接配接      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

Browser方面,我們封裝成一個updater對象:

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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+"<br>");
        }
        catch(e){
            updater.onError();
            return;
        }
        interval = window.setTimeout(updater.poll, 0);
    },
    onError: function(){
        console.log("Poll error;");
    }
};      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

要啟動長輪詢隻要調用

updater.poll();      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

可以看到,長輪詢與普通的輪詢相比更有效率(隻有資料更新時才傳回資料),減少不必要的帶寬的浪費;同時,長輪詢又改進了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号草案”。詳細可以參閱

文檔

。我們還是隻是在通信打開之後發送一堆随機數字,僅示範之用。

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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)      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

用戶端代碼也很簡單和直覺:

用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
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+"<br>");
        };
    }
};
wsUpdater.start();      
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)
用戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

總結:本文對Browser和Server端持續同步的方式進行了介紹,并進行了示範。在實際生産中,有一些架構。包括Java的Pushlet,NodeJS的socket.io,大家請自行查閱資料。

本文參考文章:

  1. Browser 與 Server 持續同步的作法介紹 (Polling, Comet, Long Polling, WebSocket)  (可能要FQ)
  2. Comet:基于 HTTP 長連接配接的“伺服器推”技術 

本文出自殘陽似血博文:

Browser和Server持續同步的幾種方式(jQuery+tornado示範)