關於web實時通信,通常使用長輪詢或這長連接方式進行實現。
為了能夠實際體會長輪詢,通過Ajax長輪詢實現了一個簡單的聊天程序,在此作為筆記。
長輪詢
傳統的輪詢方式是,客戶端定時(一般使用setInterval)向服務器發送Ajax請求,服務器接到請求后馬上返回響應信息。使用這種方式,無論客戶端還是服務端都比較好實現,但是會有很多無用的請求(服務器沒有有效數據的時候,也需要返回通知客戶端)。
而長輪詢是,客戶端向服務器發送Ajax請求,服務器接到請求后保持住連接,直到有新消息才返回響應信息,客戶端處理完響應信息后再向服務器發送新的請求。這樣的好處就是,在沒有數據的時候,客戶端和服務器之間不會有無用的請求。
對於使用長輪詢的實現,客戶端和服務器都有一定的要求:
- 客戶端發起請求,當接收到服務器響應(正常或異常的響應)后,需要向服務求發送新的請求,從而達到輪詢的效果
- 服務器端要能夠一直保持住客戶端的請求,直到有響應消息;同時服務器對請求的處理要支持非阻塞模式
實現
例子很簡單,客戶端使用Ajax進行輪詢請求,服務器端使用Python的gevent庫來實現了非阻塞式的響應。
客戶端
客戶端實現了一個longPolling的函數,當文檔加載完成后,就會調用這個longPolling函數。
注意Ajax請求的complete屬性設置,每次當longPolling函數中的Ajax請求結束后,又會重新通過longPolling函數向服務器發出輪詢請求。
function longPolling() { $.ajax({ url: "update", data: {"cursor": cursor}, type: "POST", error: function (XMLHttpRequest, textStatus, errorThrown) { $("#state").append("[state: " + textStatus + ", error: " + errorThrown + " ]<br/>"); }, success: function (result, textStatus) { msg_data = eval("(" + result + ")"); $("#inbox").append(msg_data.html); cursor = msg_data.latest_cursor; console.log(msg_data) $("#message").val(""); $("#state").append("[state: " + textStatus + " ]<br/>"); }, complete: longPolling }); }
服務端
服務器端通過MessageBuffer類來維護了一個cache(用list實現),用來存放所有來自客戶端的消息。當消息的數量超過cache_size的時候,服務器會清理掉早期的消息。
class MessageBuffer(object): def __init__(self, cache_size = 200): self.cache = [] self.cache_size = cache_size self.message_event = Event()
由於Python自帶的WSGI服務器是阻塞模式的,所以這里使用了gevent庫中提供的非阻塞模式的WSGI服務器。
服務器的工作流程可以簡單描述如下:
-
當服務器接收到客戶端的數據請求時(/update)
- 如果存放消息cache為空,或者客戶端已經得到了最新的消息(根據cursor這個GUID來判斷),服務器阻塞(保持)該請求
- 服務器將所有有效的消息返回給客戶端
- 當服務器接收到新的消息時(/new請求),服務器將新消息添加到cache中,並通過message_event事件來喚醒被阻塞的update請求
def application(env, start_response): # visit the main page if env['PATH_INFO'] == '/': return generate_response_data('200 OK', chat_html, start_response) # client to send a new message elif env['PATH_INFO'] == '/new': msg = escape(get_request_data("msg", env)) msg_item = {} msg_item["id"] = str(uuid.uuid4()) msg_item["msg"] = msg print "Got new message from client %s" %str(msg_item) messageBuffer.cache.append(msg_item) if len(messageBuffer.cache) > messageBuffer.cache_size: messageBuffer.cache = messageBuffer.cache[-messageBuffer.cache_size:] messageBuffer.message_event.set() messageBuffer.message_event.clear() return generate_response_data('200 OK', "", start_response) # serve to send available messages elif env['PATH_INFO'] == '/update': cursor = escape(get_request_data("cursor", env)) print "cursor: %s" %cursor # if message buffer is empty or no new messages, just wait if len(messageBuffer.cache) == 0 or messageBuffer.cache[-1]["id"] == cursor: messageBuffer.message_event.wait() for index, m in enumerate(messageBuffer.cache): if m['id'] == cursor: return generate_response_data('200 OK', generate_json_data(messageBuffer.cache[index + 1:]), start_response) return generate_response_data('200 OK', generate_json_data(messageBuffer.cache), start_response) else: return generate_response_data('404 Not Found', b'<h1>Not Found</h1>', start_response)
運行效果
通過下面兩個圖片可以看到運行效果。
長輪詢也是長連接?
為了進一步看看長輪詢的工作方式,基於上面的例子,通過wireshark抓取了一些數據包。
在服務器啟動后,客戶端發送了三條消息,並通過三次"/update"請求分別得到了這三條消息。
從截圖中可以看到,三次請求並沒有創建新的連接,而是重用了TCP連接;這是因為HTTP 1.1中會有一個"keep-alive"模式,服務器響應后並不會直接關閉TCP連接,而是看看客戶端會不會有新的請求,從而重用已有的TCP連接。
對於客戶端,由於每次收到消息后都會重新發送新的"/update"請求,所以可以一直重用TCP連接。
當客戶端發出"/update"請求后,如果沒有新的消息可以返回,服務器會一直保持這個請求。
為了保持住這條TCP連接,可以看到客戶端會定期的發送"TCP Keep-Alive"包來維持TCP連接。
我的理解是,在使用HTTP的"keep-alive"模式中,長輪詢中始終使用的是相同的TCP連接,其實這也是一種長連接方式。
總結
本文中,通過簡單的例子試用了長輪詢的方式來實現web實時通信。
長輪詢的方式,使用起來相對容易,同時用能減少客戶端和服務器之間的無用請求。
Ps:
通過此處可以下載例子的源碼,需要安裝Python和gevent才能正常運行。