1. 程式人生 > >客戶端與伺服器持續同步解析(輪詢,comet,WebSocket)

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

作者:盼逆邵年  來源:部落格園  釋出時間:2012-02-10 20:42  閱讀:1943 次  原文連結[收藏]

在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或者從資料庫中獲取內容。

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,即readystate==3時(資料仍在傳輸),客戶端可以讀取資料,而不用關閉連線。問題也在這裡,IE 在 readystate 為 3 時,不能讀取伺服器返回的資料,所以目前 IE 不支援基於 Streaming AJAX,而長輪詢由於是普通的AJAX請求,所以沒有瀏覽器相容問題。另外,由於使用streaming方式,控制權在伺服器端,並且在長連線期間,並沒有客戶端到伺服器端的資料,所以不能根據客戶端的資料進行即時的適應(比如檢查cookie等等),而對於long polling方式,在每次斷開連線之後可以進行判斷。所以綜合來說,long polling是現在比較主流的做法(如fb,Plurk)。

接下來,我們就來對流(streaming)和長輪詢(long-polling)兩種方式進行演示。

流(streaming)方式

從上圖可以看出每次資料傳送不會關閉連線,連線只會在通訊出現錯誤時,或是連線重建時關閉(一些防火牆常被設定為丟棄過長的連線, 伺服器端可以設定一個超時時間, 超時後通知客戶端重新建立連線,並關閉原來的連線)。

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

伺服器端程式碼像這樣:

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)
)

對於伺服器端,仍然是生成隨機數字,由於要不斷輸出資料,於是在回撥函式裡延遲3秒,然後繼續呼叫get_data方法。在這裡要注意的是,不能使用time.sleep(),由於tornado是單執行緒的,使用sleep方法會block主執行緒。因此要呼叫IOLoop的add_timeout方法(引數0:執行時間戳,引數1:回撥函式)。於是伺服器端會生成一個隨機數字,延遲3秒再生成隨機數字,迴圈往復。
 

於是前端js就是:

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);


對於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(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)
)


在客戶端我們只需定義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(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方法斷開連線


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(data+"<br>");
}
catch(e){
updater.onError();
return;
}
interval = window.setTimeout(updater.poll, 0);
},
onError: function(){
console.log("Poll error;");
}
};


要啟動長輪詢只要呼叫

updater.poll();


可以看到,長輪詢與普通的輪詢相比更有效率(只有資料更新時才返回資料),減少不必要的頻寬的浪費;同時,長輪詢又改進了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號草案”。詳細可以參閱文件。我們還是隻是在通訊開啟之後傳送一堆隨機數字,僅演示之用。

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)

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

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();

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

本文參考文章: