數(shù)據(jù)不夠?qū)崟r:試試長連接?
背景
在特定場景下,我們往往需要實時的去獲取最新的數(shù)據(jù),如獲取消息推送或公告、聊天消息、實時的日志和學(xué)情等,都對數(shù)據(jù)的實時性要求很高,面對這類場景,最常用的可能就是輪詢,但除了輪詢還有長連接(Websocket)和服務(wù)端推送(SSE)方案可供選擇。
輪詢
輪詢就是采用循環(huán)http請求的方式,通過重復(fù)的接口請求去獲取最新的數(shù)據(jù)。
短輪詢 (polling)
短輪詢可能是我們用的最多的一種實時刷新數(shù)據(jù)的方式了,我們在講輪詢方案時,大部分指的就是短輪詢,其實現(xiàn)方式和普通的接口無異,改造也只要前端增加定時器或useRequest配置輪詢參數(shù)即可,其原理也非常簡單,如下圖,如果是http1.1及以上,TCP連接可以復(fù)用,當(dāng)然http1.0及以下也是可以使用,但消耗會更多。短輪詢的特點就是接口請求立即會返回,每次請求都可以理解為是一次新的請求。
短輪詢的優(yōu)缺點
短輪詢最大的優(yōu)點就是簡單,前端設(shè)置時間間隔,定時去請求數(shù)據(jù),而服務(wù)端只需同步的查詢數(shù)據(jù)返回即可,但缺點也顯而易見:
- 無用請求過多:從下圖可以看出,每隔固定時間,一定有請求發(fā)出,且每次接口可能返回一樣的數(shù)據(jù)或返回空結(jié)果,服務(wù)端會重復(fù)查詢數(shù)據(jù)庫、前端會重復(fù)重渲染
- 實時性不可控,如數(shù)據(jù)更新了,但輪詢請求剛結(jié)束一輪,會造成輪詢間隔內(nèi)數(shù)據(jù)都得不到更新
長輪詢 (long polling)
看完了上面關(guān)于短輪詢的介紹,我們知道了輪詢有兩個主要的缺陷:一個是無用請求過多,另外一個是數(shù)據(jù)實時性不可控。為了解決這兩個問題,于是有了更進一步的長輪詢方案。
在上圖中,客戶端發(fā)起請求后,服務(wù)端發(fā)現(xiàn)當(dāng)前沒有新的數(shù)據(jù),這個時候服務(wù)端沒有立即返回請求,而是將請求掛起,在等待一段時間后(一般為30s或者是60s,設(shè)置一個超時返回主要是為了考慮過長的無數(shù)據(jù)連接占用會被網(wǎng)關(guān)或者某層中間件斷開甚至是被運營商斷開),如發(fā)現(xiàn)還是沒有數(shù)據(jù)更新的話,就返回一個空結(jié)果給客戶端??蛻舳嗽谑盏椒?wù)端的回復(fù)后,立即再次向服務(wù)端發(fā)送新的請求。這次服務(wù)端在接收到客戶端的請求后,同樣等待了一段時間,這次好運的是服務(wù)端的數(shù)據(jù)發(fā)生了更新,服務(wù)端給客戶端返回了最新的數(shù)據(jù)??蛻舳嗽谀玫浇Y(jié)果后再次發(fā)送下一個請求,如此反復(fù)。
長輪詢的優(yōu)缺點
長輪詢很完美地解決了短輪詢的問題,首先服務(wù)端在沒有數(shù)據(jù)更新的情況下沒有給客戶端返回數(shù)據(jù),所以避免了客戶端大量的重復(fù)請求。再者客戶端在收到服務(wù)端的返回后,馬上發(fā)送下一個請求,這就保證了更好的數(shù)據(jù)實時性。不過長輪詢也有缺點:
- 服務(wù)端資源大量消耗: 服務(wù)端會一直hold住客戶端的請求,這部分請求會占用服務(wù)器的資源。對于某些語言來說,每一個HTTP連接都是一個獨立的線程,過多的HTTP連接會消耗掉服務(wù)端的內(nèi)存資源。
- 難以處理數(shù)據(jù)更新頻繁的情況: 如果數(shù)據(jù)更新頻繁,會有大量的連接創(chuàng)建和重建過程,這部分消耗是很大的。雖然HTTP有TCP連接復(fù)用,但每次拿到數(shù)據(jù)后客戶端都需要重新請求,因此相對于WebSocket和SSE它多了一個發(fā)送新請求的階段,對實時性和性能還是有影響的。
從上面的描述來看,長輪詢的次數(shù)和時延似乎可以更少,那是不是長輪詢更好呢?其實不是的,這個兩種輪詢方式都有優(yōu)劣勢和適合的場景。
短輪詢 ,長輪詢怎么選?
長 輪詢多用于操作頻繁,點對點的通訊,而且連接數(shù)不能太多情況,每個TCP連接都需要三步握手,這需要時間,如果每個操作都是先連接,再操作的話那么處理速度會降低很多,所以每個操作完后都不斷開,次處理時直接發(fā)送數(shù)據(jù)包就OK了,不用建立TCP連接。例如:數(shù)據(jù)庫的連接用長連接, 如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創(chuàng)建也是對資源的浪費。
而像WEB網(wǎng)站的http服務(wù)一般都用短 輪詢,因為長連接對于服務(wù)端來說會耗費一定的資源,而像WEB網(wǎng)站這么頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源,如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,那可想而知吧。所以并發(fā)量大,但每個用戶無需頻繁操作情況下需用短連好。
長連接
?WebSocket?
上面說到長輪詢不適用于服務(wù)端資源頻繁更新的場景,而解決這類問題的一個方案就是WebSocket。用最簡單的話來介紹WebSocket就是:客戶端和服務(wù)器之間建立一個持久的長連接,這個連接是雙工的,客戶端和服務(wù)端都可以實時地給對方發(fā)送消息。下面是WebSocket的圖示:
WebSocket對于前端的同學(xué)來說是非常常見了,因為無論是webpack還是vite,用來HMR的reload就是通過WebSocket來進行的,有代碼改動,工程重新編譯,新變更的模塊通知到瀏覽器加載新的模塊,這里的通知瀏覽器加載新模塊就是通過WebSocket的進行的。如上圖,通過握手(協(xié)議轉(zhuǎn)換)建立連接后,雙方就保持持久連接,由于歷史的關(guān)系,WebSocket建立連接是依賴HTTP的,但是其建連請求有明顯的特征,目的是客戶端和服務(wù)端都能識別并保持連接。
請求特征
請求頭特征
- HTTP 必須是 1.1 GET 請求
- HTTP Header 中 Connection 字段的值必須為 Upgrade
- HTTP Header 中 Upgrade 字段必須為 websocket
- Sec-WebSocket-Key 字段的值是采用 base64 編碼的隨機 16 字節(jié)字符串
- Sec-WebSocket-Protocol 字段的值記錄使用的子協(xié)議,比如 binary base64
- Origin 表示請求來源
響應(yīng)頭特征
- 狀態(tài)碼是 101 表示 Switching Protocols
- Upgrade / Connection / Sec-WebSocket-Protocol 和請求頭一致
- Sec-WebSocket-Accept 是通過請求頭的 Sec-WebSocket-Key 生成
兼容性
WebSocket 協(xié)議在2008年誕生,2011年成為國際標(biāo)準(zhǔn)?,F(xiàn)在所有瀏覽器都已經(jīng)支持了。
實現(xiàn)一個簡單的 WebSocket
基于原生WebSocket我們實現(xiàn)一個簡單的長連。
連接
發(fā)送消息
監(jiān)聽消息
關(guān)閉連接
?在工程上使用WebSocket?
在工程上,很少直接基于原生WebSocket實現(xiàn)業(yè)務(wù)需求,使用WebSocket需要完成下面幾個問題:
- 鑒權(quán):防止惡意連接連接進來接收消息
- 心跳:客戶端意外斷開,導(dǎo)致死鏈占用服務(wù)端資源,長時間無消息的連接可能會被中間網(wǎng)關(guān)或運營商斷開
- 登錄:通過建連需要識別出該連接是哪個用戶,有無權(quán)限,需要推送哪些消息
- 日志:監(jiān)控連接,錯誤上報
- 后臺:能方便的查看在線連接的客戶端數(shù)量,消息傳輸量
服務(wù)端推送(SSE)
SSE全稱Server-sent Events,是HTML 5 規(guī)范的一個組成部分,該規(guī)范十分簡單,主要由兩個部分組成:第一個部分是服務(wù)器端與瀏覽器端之間的通訊協(xié)議,第二部分則是在瀏覽器端可供 JavaScript 使用的 EventSource 對象。通訊協(xié)議是基于純文本的簡單協(xié)議。服務(wù)器端的響應(yīng)的內(nèi)容類型是“text/event-stream”。響應(yīng)文本的內(nèi)容可以看成是一個事件流,由不同的事件所組成。每個事件由類型和數(shù)據(jù)兩部分組成,同時每個事件可以有一個可選的標(biāo)識符。不同事件的內(nèi)容之間通過僅包含回車符和換行符的空行(“rn”)來分隔。每個事件的數(shù)據(jù)可能由多行組成。
?和 Websocket對比?
SSE | WebSocket |
單向:僅服務(wù)端能發(fā)送消息 | 雙向:客戶端、服務(wù)端雙向發(fā)送 |
僅文本數(shù)據(jù) | 二進制、文本都可 |
常規(guī)HTTP協(xié)議 | WebSocket協(xié)議 |
?兼容性?
?數(shù)據(jù)格式?
服務(wù)器向瀏覽器發(fā)送的 SSE 數(shù)據(jù),必須是 UTF-8 編碼的文本,
響應(yīng)頭
數(shù)據(jù)傳輸
服務(wù)端每次發(fā)送消息,由若干message?組成,使用\n\n?分隔,如果單個messag?過長,可以用\n分隔。
field取值
例子
?實現(xiàn)一個簡單的SSE?
web端
實例化EventSource?,監(jiān)聽open、message、error
服務(wù)端
以nodejs為例,服務(wù)端代碼和普通請求無異,并沒有新的處理類庫。
和WebSocket不同,SSE并不是新的通信協(xié)議,其本質(zhì)是在普通HTTP請求的基礎(chǔ)上定義一個Content-Type?,保持上連接,通過普通的接口也能模擬出SSE的效果,以XMLHttpRequest為例
參考文獻
rfc6455.pdf[1]
WebSocket協(xié)議中文版(rfc6455)[2]
深入剖析WebSocket的原理 - 知乎[3]
HTTP長連接實現(xiàn)原理 - 掘金[4]
WebSocket() - Web API 接口參考 | MDN[5]
EventSource - Web API 接口參考 | MDN[6]?