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

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

在開始以前,爲了讓你們有個清晰的認識,首先列出本文所要講到的內容大概。本文將會分如下幾部分: html

  1. 普通的輪詢(Polling)
  2. Comet:基於服務器長鏈接的「服務器推」技術。這其中又分爲兩種:
    1. 基於AJAX和基於IFrame的流(streaming)方式
    2. 基於AJAX的長輪詢(long-polling)方式
  3. WebSocket

古老的輪詢

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

?
1
2
3
4
5
6
varpolling =function(){
    $.post('/polling',function(data, textStatus){
        $("p").append(data+"<br>");
    });
};
interval = setInterval(polling, 1000);

後端咱們只是象徵性地隨機生成一些數字,而且返回。在實際應用中能夠訪問cache或者從數據庫中獲取內容。 html5

?
1
2
3
4
5
6
7
importrandom
importtornado.web
 
classPollingHandler(tornado.web.RequestHandler):
    defpost(self):
        num=random.randint(1,100)
        self.write(str(num))

polling

能夠看到,採用polling的方式,效率是十分低下的,一方面,服務器端不是總有數據更新,因此每次問詢不必定都有更新,效率低下;另外一方面,當發起請求的客戶端數量增長,服務器端的接受的請求數量會大量上升,無形中就增長了服務器的壓力。 web

Comet:基於HTTP長鏈接的「服務器推」技術

看到 這個標題有的人可能就暈了,其實原理仍是比較簡單的。基於Comet的技術主要分爲流(streaming)方式和長輪詢(long-polling)方式。 ajax

首先看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)方式 安全

streaming

從上圖能夠看出每次數據傳送不會關閉鏈接,鏈接只會在通訊出現錯誤時,或是鏈接重建時關閉(一些防火牆常被設置爲丟棄過長的鏈接, 服務器端能夠設置一個超時時間, 超時後通知客戶端從新創建鏈接,並關閉原來的鏈接)。

流方式首先一種經常使用的作法是使用AJAX的流方式(如先前所說,此方法主要判斷readystate==3時的狀況,因此不能適用於IE)。

服務器端代碼像這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
classStreamingHandler(tornado.web.RequestHandler):
    '''使用asynchronus裝飾器使得post方法變成無阻塞'''
    @tornado.web.asynchronous
    defpost(self):
        self.get_data(callback=self.on_finish)
             
    defget_data(self, callback):
        ifself.request.connection.stream.closed():
            return
             
        num=random.randint(1,100)#生成隨機數
        callback(num)#調用回調函數
             
    defon_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就是:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
try{
    varrequest =newXMLHttpRequest();
}catch(e) {
    alert("Browser doesn't support window.XMLHttpRequest");
}
                         
varpos = 0;
request.onreadystatechange =function() {
    if(request.readyState === 3) {//在 Interactive 模式處理
        vardata = 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數據結束位置。

streaming

另一種經常使用方法是使用IFrame的streaming方式,這也是早先的經常使用作法。首先咱們在頁面裏放置一 個iframe,它的src設置爲一個長鏈接的請求地址。Server端的代碼基本一致,只是輸出的格式改成HTML,用來輸出一行行的Inline Javascript。因爲輸出就獲得執行,所以就少了存儲遊標(pos)的過程。服務器端代碼像這樣:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
classIframeStreamingHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    defget(self):
        self.get_data(callback=self.on_finish)
             
    defget_data(self, callback):
        ifself.request.connection.stream.closed():
            return
             
        num=random.randint(1,100)
        callback(num)
             
    defon_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函數:

?
1
2
3
varadd_content =function(str){
    $("p").append(str);
};

iframe streaming

由此能夠看出,採用IFrame的streaming方式解決了瀏覽器兼容問題。可是因爲傳統的Web服務器每次鏈接都會佔用一個鏈接線程,這樣隨 着增長的客戶端長鏈接到服務器時,線程池裏的線程最終也就會用光。所以,Comet長鏈接只有對於非阻塞異步Web服務器纔會產生做用。這也是爲何選擇 tornado的緣由。

使用iframe方式一個問題就是瀏覽器會一直處於加載狀態。

長輪詢(long-polling)方式

long polling

長輪詢是如今最爲經常使用的方式,和流方式的區別就是服務器端在接到請求後掛起,有更新時返回鏈接即斷掉,而後客戶端再發起新的鏈接。因而Server端代碼就簡單好多,和上面的任務相似:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
classLongPollingHandler(tornado.web.RequestHandler):
    @tornado.web.asynchronous
    defpost(self):
        self.get_data(callback=self.on_finish)
             
    defget_data(self, callback):
        ifself.request.connection.stream.closed():
            return
             
        num=random.randint(1,100)
        tornado.ioloop.IOLoop.instance().add_timeout(
            time.time()+3,
            lambda: callback(num)
        )# 間隔3秒調用回調函數
             
    defon_finish(self, data):
        self.write("Server says: %d"%data)
        self.finish()# 使用finish方法斷開鏈接

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

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
varupdater = {
    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;");
    }
};

要啓動長輪詢只要調用

?
1
updater.poll();

long polling

能夠看到,長輪詢與普通的輪詢相比更有效率(只有數據更新時才返回數據),減小沒必要要的帶寬的浪費;同時,長輪詢又改進了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號草案」。詳細能夠參閱文檔。咱們仍是隻是在通訊打開以後發送一堆隨機數字,僅演示之用。

?
1
2
3
4
5
6
7
8
9
10
11
importtornado.websocket
 
classWebSocketHandler(tornado.websocket.WebSocketHandler):
    defopen(self):
        foriinxrange(10):
            num=random.randint(1,100)
            self.write_message(str(num))
         
    defon_message(self, message):
        logging.info("getting message %s", message)
        self.write_message("You say:"+message)

客戶端代碼也很簡單和直觀:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
varwsUpdater = {
    socket:null,
    start:function(){
        if("WebSocket"inwindow) {
            wsUpdater.socket =newWebSocket(" ws://localhost:8889/websocket");
        }
        else{
            wsUpdater.socket =newMozWebSocket(" ws://localhost:8889/websocket");
        }
        wsUpdater.socket.onmessage =function(event) {
            $("p").append(event.data+"<br>");
        };
    }
};
wsUpdater.start();

WebSocket

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

本文參考文章:

  1. Browser 與 Server 持續同步的做法介紹 (Polling, Comet, Long Polling, WebSocket) (可能要FQ)
  2. Comet:基於 HTTP 長鏈接的「服務器推」技術 
相關文章
相關標籤/搜索