close
大廠技術堅持周更精選好文

本文為來自 字節教育-智能學習-前端團隊 的文章,已授權 ELab 發布。

智能學習前端團隊 自創立以來,團隊專注於打破大眾對教育的刻板印象,突破固有的教學思維,攻破各類教學屏障。旨在為每一位學生制定最合適的學習方案,予以因材施教,使優質教育隨『觸』可達。

背景

在特定場景下,我們往往需要實時的去獲取最新的數據,如獲取消息推送或公告、股票大盤、聊天消息、實時的日誌和學情等,都對數據的實時性要求很高,面對這類場景,最常用的可能就是輪詢,但除了輪詢還有長連接(Websocket)和服務端推送(SSE)方案可供選擇。

輪詢

輪詢就是採用循環http請求的方式,通過重複的接口請求去獲取最新的數據。

短 輪詢 (polling)

短輪詢可能是我們用的最多的一種實時刷新數據的方式了,我們在講輪詢方案時,大部分指的就是短輪詢,其實現方式和普通的接口無異,改造也只要前端增加定時器或useRequest配置輪詢參數即可,其原理也非常簡單,如下圖,如果是http1.1及以上,TCP連接可以復用,當然http1.0及以下也是可以使用,但消耗會更多。短輪詢的特點就是接口請求立即會返回,每次請求都可以理解為是一次新的請求。

image.png

短 輪詢 的優缺點

短輪詢最大的優點就是簡單,前端設置時間間隔,定時去請求數據,而服務端只需同步的查詢數據返回即可,但缺點也顯而易見:

無用請求過多:從下圖可以看出,每隔固定時間,一定有請求發出,且每次接口可能返回一樣的數據或返回空結果,服務端會重複查詢數據庫、前端會重複重渲染
實時性不可控,如數據更新了,但輪詢請求剛結束一輪,會造成輪詢間隔內數據都得不到更新

長 輪詢 (long polling)

看完了上面關於短輪詢的介紹,我們知道了輪詢有兩個主要的缺陷:一個是無用請求過多,另外一個是數據實時性不可控。為了解決這兩個問題,於是有了更進一步的長輪詢方案。

image.png

在上圖中,客戶端發起請求後,服務端發現當前沒有新的數據,這個時候服務端沒有立即返回請求,而是將請求掛起,在等待一段時間後(一般為30s或者是60s,設置一個超時返回主要是為了考慮過長的無數據連接占用會被網關或者某層中間件斷開甚至是被運營商斷開),如發現還是沒有數據更新的話,就返回一個空結果給客戶端。客戶端在收到服務端的回覆後,立即再次向服務端發送新的請求。這次服務端在接收到客戶端的請求後,同樣等待了一段時間,這次好運的是服務端的數據發生了更新,服務端給客戶端返回了最新的數據。客戶端在拿到結果後再次發送下一個請求,如此反覆。

長 輪詢 的優缺點

長輪詢很完美地解決了短輪詢的問題,首先服務端在沒有數據更新的情況下沒有給客戶端返回數據,所以避免了客戶端大量的重複請求。再者客戶端在收到服務端的返回後,馬上發送下一個請求,這就保證了更好的數據實時性。不過長輪詢也有缺點:

服務端資源大量消耗: 服務端會一直hold住客戶端的請求,這部分請求會占用服務器的資源。對於某些語言來說,每一個HTTP連接都是一個獨立的線程,過多的HTTP連接會消耗掉服務端的內存資源。
難以處理數據更新頻繁的情況: 如果數據更新頻繁,會有大量的連接創建和重建過程,這部分消耗是很大的。雖然HTTP有TCP連接復用,但每次拿到數據後客戶端都需要重新請求,因此相對於WebSocket和SSE它多了一個發送新請求的階段,對實時性和性能還是有影響的。

從上面的描述來看,長輪詢的次數和時延似乎可以更少,那是不是長輪詢更好呢?其實不是的,這個兩種輪詢方式都有優劣勢和適合的場景。

短 輪詢 ,長輪詢怎麼選?

長 輪詢多用於操作頻繁,點對點的通訊,而且連接數不能太多情況,每個TCP連接都需要三步握手,這需要時間,如果每個操作都是先連接,再操作的話那麼處理速度會降低很多,所以每個操作完後都不斷開,次處理時直接發送數據包就OK了,不用建立TCP連接。例如:數據庫的連接用長連接, 如果用短連接頻繁的通信會造成socket錯誤,而且頻繁的socket 創建也是對資源的浪費。

而像WEB網站的http服務一般都用短 輪詢,因為長連接對於服務端來說會耗費一定的資源,而像WEB網站這麼頻繁的成千上萬甚至上億客戶端的連接用短連接會更省一些資源,如果用長連接,而且同時有成千上萬的用戶,如果每個用戶都占用一個連接的話,那可想而知吧。所以並發量大,但每個用戶無需頻繁操作情況下需用短連好。

長連接 WebSocket

上面說到長輪詢不適用於服務端資源頻繁更新的場景,而解決這類問題的一個方案就是WebSocket。用最簡單的話來介紹WebSocket就是:客戶端和服務器之間建立一個持久的長連接,這個連接是雙工的,客戶端和服務端都可以實時地給對方發送消息。下面是WebSocket的圖示:

image.png

WebSocket對於前端的同學來說是非常常見了,因為無論是webpack還是vite,用來HMR的reload就是通過WebSocket來進行的,有代碼改動,工程重新編譯,新變更的模塊通知到瀏覽器加載新的模塊,這裡的通知瀏覽器加載新模塊就是通過WebSocket的進行的。如上圖,通過握手(協議轉換)建立連接後,雙方就保持持久連接,由於歷史的關係,WebSocket建立連接是依賴HTTP的,但是其建連請求有明顯的特徵,目的是客戶端和服務端都能識別並保持連接。

請求特徵

請求頭特徵

HTTP 必須是 1.1 GET 請求
HTTP Header 中 Connection 字段的值必須為 Upgrade
HTTP Header 中 Upgrade 字段必須為 websocket
Sec-WebSocket-Key 字段的值是採用 base64 編碼的隨機 16 字節字符串
Sec-WebSocket-Protocol 字段的值記錄使用的子協議,比如 binary base64
Origin 表示請求來源

響應頭特徵

狀態碼是 101 表示 Switching Protocols
Upgrade / Connection / Sec-WebSocket-Protocol 和請求頭一致
Sec-WebSocket-Accept 是通過請求頭的 Sec-WebSocket-Key 生成
兼容性

WebSocket 協議在2008年誕生,2011年成為國際標準。現在所有瀏覽器都已經支持了。

實現一個簡單的 WebSocket

基於原生WebSocket我們實現一個簡單的長連。

連接

//連接只需實例一個WebSocketconstws=newWebSocket(`wss://${url}`);

發送消息

ws.send("這是一條消息:"+count);

監聽消息

ws.onmessage=function(event){console.log(event.data);}

關閉連接

ws.close(); 在工程上使用WebSocket

在工程上,很少直接基於原生WebSocket實現業務需求,使用WebSocket需要完成下面幾個問題:

鑒權:防止惡意連接連接進來接收消息
心跳:客戶端意外斷開,導致死鏈占用服務端資源,長時間無消息的連接可能會被中間網關或運營商斷開
登錄:通過建連需要識別出該連接是哪個用戶,有無權限,需要推送哪些消息
日誌:監控連接,錯誤上報
後台:能方便的查看在線連接的客戶端數量,消息傳輸量
服務端推送(SSE)

SSE全稱Server-sent Events,是HTML 5 規範的一個組成部分,該規範十分簡單,主要由兩個部分組成:第一個部分是服務器端與瀏覽器端之間的通訊協議,第二部分則是在瀏覽器端可供 JavaScript 使用的 EventSource 對象。通訊協議是基於純文本的簡單協議。服務器端的響應的內容類型是「text/event-stream」。響應文本的內容可以看成是一個事件流,由不同的事件所組成。每個事件由類型和數據兩部分組成,同時每個事件可以有一個可選的標識符。不同事件的內容之間通過僅包含回車符和換行符的空行(「rn」)來分隔。每個事件的數據可能由多行組成。

image.png 和 Websocket對比
SSEWebSocket單向:僅服務端能發送消息雙向:客戶端、服務端雙向發送僅文本數據二進制、文本都可常規HTTP協議WebSocket協議
兼容性 數據格式

服務器向瀏覽器發送的 SSE 數據,必須是 UTF-8 編碼的文本,

響應頭

Content-Type:text/event-streamCache-Control:no-cacheConnection:keep-alive

數據傳輸

服務端每次發送消息,由若干message組成,使用\n\n分隔,如果單個messag過長,可以用\n分隔。

field取值

dataeventidretry

例子

//注釋,用於心跳包:thisisateststream\n\n//設置斷鏈1000ms重試一次retry:1000\n\nevent:自定義消息\n\ndata:sometext\n\ndata:anothermessage\ndata:withtwolines\n\n 實現一個簡單的SSE

web端

實例化EventSource,監聽open、message、error

constsource=newEventSource(url,{withCredentials:true});//監聽消息source.onmessage=function(event){//handlemessage};source.addEventListener('message',function(event){//handlemessage},false);//監聽錯誤source.onerror=function(event){//handleerror};source.addEventListener('error',function(event){//handleerror},false);//關閉連接source.close()

服務端

以nodejs為例,服務端代碼和普通請求無異,並沒有新的處理類庫。

res.writeHead(200,{"Content-Type":"text/event-stream","Cache-Control":"no-cache","Connection":"keep-alive","Access-Control-Allow-Origin":'*',});res.write("retry:10000\n\n");res.write("event:connecttime\n\n");res.write("data:"+(newDate())+"\n");res.write("data:"+(newDate())+"\n\n");//模擬收到消息推送給客戶端interval=setInterval(function(){res.write("data:"+(newDate())+"\n\n");},1000);

和WebSocket不同,SSE並不是新的通信協議,其本質是在普通HTTP請求的基礎上定義一個Content-Type,保持上連接,通過普通的接口也能模擬出SSE的效果,以XMLHttpRequest為例

constxhr=newXMLHttpRequest();xhr.open("GET","http://localhost:8844/long",true);xhr.onload=(e)=>{console.log("onload",xhr.responseText);};xhr.onprogress=(e)=>{//每次服務端寫入response的數據,都會傳輸過來,並產生一次onprogress事件console.log("onprogress",xhr.responseText);};xhr.send();參考文獻

rfc6455.pdf[1]

WebSocket協議中文版(rfc6455)[2]

深入剖析WebSocket的原理 - 知乎[3]

HTTP長連接實現原理 - 掘金[4]

WebSocket() - Web API 接口參考 | MDN[5]

EventSource - Web API 接口參考 | MDN[6]

❤️ 謝謝支持

以上便是本次分享的全部內容,希望對你有所幫助^_^

喜歡的話別忘了 分享、點讚、收藏 三連哦~。

歡迎關注公眾號 ELab團隊 收貨大廠一手好文章~

智能學習前端團隊 自創立以來,團隊專注於打破大眾對教育的刻板印象,突破固有的教學思維,攻破各類教學屏障。旨在為每一位學生制定最合適的學習方案,予以因材施教,使優質教育隨『觸』可達。

字節跳動校/社招內推碼: ERSK6WM
投遞鏈接: https://job.toutiao.com/s/k4H1XUJ

可憑內推碼投遞 字節教育 - 智能學習前端團隊 相關崗位哦~

參考資料
[1]

rfc6455.pdf: https://datatracker.ietf.org/doc/pdf/rfc6455.pdf

[2]

WebSocket協議中文版(rfc6455): https://blog.csdn.net/aigoogle/article/details/122281445

[3]

深入剖析WebSocket的原理 - 知乎: https://zhuanlan.zhihu.com/p/32845970

[4]

HTTP長連接實現原理 - 掘金: https://juejin.cn/post/6923887573861564423

[5]

WebSocket() - Web API 接口參考 | MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/WebSocket/WebSocket

[6]

EventSource - Web API 接口參考 | MDN: https://developer.mozilla.org/zh-CN/docs/Web/API/EventSource

- END -
arrow
arrow
    全站熱搜
    創作者介紹
    創作者 鑽石舞台 的頭像
    鑽石舞台

    鑽石舞台

    鑽石舞台 發表在 痞客邦 留言(0) 人氣()