使用Ajax long polling實現簡單的聊天程序


關於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才能正常運行。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM