1. 程式人生 > >實時訊息推送整理

實時訊息推送整理

分不清輪詢、長輪詢?不知道什麼時候該用websocket還是SSE,看這篇就夠了。 所謂的“實時推送”,從表面意思上來看是,客戶端訂閱的內容在發生改變時,伺服器能夠實時地通知客戶端,進而客戶端進行相應地反應。客戶端不需要主觀地傳送請求去獲取自己關心的內容,而是由伺服器端進行“推送”。 注意上面的推送二字打了引號,這就意味著在現有的幾種實現方式中,並不是伺服器端主動地推送,而是通過一定的手段營造了一種實時的假象。就目前現有的幾種技術而言,主要有以下幾類: - 客戶端輪詢:傳統意義上的輪詢(Short Polling) - 伺服器端輪詢:長輪詢(Long Polling) - 全雙工通訊:Websocket - 單向伺服器推送:Server-Sent Events(SSE) 文中會以一個簡易聊天室的例子來分別通過上述的四種方式實現,程式碼地址[mini-chatroom](https://github.com/Rynxiao/mini-chatroom)(存在些許bug,主要是為了做演示用) ![overview](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016105757468-305314082.gif) ## 輪詢(Short Polling) 輪詢的實現原理:客戶端向伺服器端傳送一個請求,伺服器返回資料,然後客戶端根據伺服器端返回的資料進行處理;然後客戶端繼續向伺服器端傳送請求,繼續重複以上的步驟,如果不想給伺服器端太大的壓力,一般情況下會設定一個請求的時間間隔。 ![shortPolling](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016105818016-1902321969.png) 使用輪詢明顯的優點是基礎不需要額外的開發成本,請求資料,解析資料,作出響應,僅此而已,然後不斷重複。缺點也顯而易見: - 不斷的傳送和關閉請求,對伺服器的壓力會比較大,因為本身開啟Http連線就是一件比較耗資源的事情 - 輪詢的時間間隔不好控制。如果要求的實時性比較高,顯然使用短輪詢會有明顯的短板,如果設定interval的間隔過長,會導致訊息延遲,而如果太短,會對伺服器產生壓力 ### 程式碼實現 ```javascript var ShortPollingNotification = { datasInterval: null, subscribe: function() { this.datasInterval = setInterval(function() { Request.getDatas().then(function(res) { window.ChatroomDOM.renderData(res); }); }, TIMEOUT); return this.unsubscribe; }, unsubscribe: function() { this.datasInterval && clearInterval(this.datasInterval); } } ``` ![shortPolling](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016105918505-929368309.gif) 下面是對應的請求,注意左下角的請求數量一直在變化 ![shortNetwork](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016105939851-33410716.gif) 在上圖中,每隔1s就會發送一個請求,看起來效果還不錯,但是如果將timeout的值設定成5s,效果將大打折扣,如圖: ![shortPolling5s](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016110009244-1365730063.gif) ## 長輪詢(Long Polling) 長輪詢的基本原理:客戶端傳送一個請求,伺服器會hold住這個請求,直到監聽的內容有改變,才會返回資料,斷開連線,客戶端繼續傳送請求,重複以上步驟。或者在一定的時間內,請求還得不到返回,就會因為超時自動斷開連線。 ![longPolling](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016110027013-94184699.png) 長輪詢是基於輪詢上的改進版本,主要是減少了客戶端發起Http連線的開銷,改成了在伺服器端主動地去判斷所關心的內容是否變化,所以其實輪詢的本質並沒有多大變化,變化的點在於: - 對於內容變化的輪詢由客戶端改成了伺服器端(客戶端會在連線中斷之後,會再次傳送請求,對比短輪詢來說,大大減少了發起連線的次數) - 客戶端只會在資料改變時去作相應的改變,對比短輪詢來說,並不是全盤接收 ### 程式碼實現 ```javascript // 客戶端 var LongPollingNotification = { // .... subscribe: function() { var that = this; // 設定超時時間 Request.getV2Datas(this.getKey(),{ timeout: 10000 }).then(function(res) { var data = res.data; window.ChatroomDOM.renderData(res); // 成功獲取資料後會再次傳送請求 that.subscribe(); }).catch(function (error) { // timeout 之後也會再次傳送請求 that.subscribe(); }); return this.unsubscribe; } // .... } ``` 筆者採用的是express,預設不支援hold住請求,因此用了一個express-longpoll的庫來實現。 下面是一個原生不用庫的實現(這裡只是介紹原理),整體的思路是:如果伺服器端支援hold住請求的話,那麼在一定的時間內會自輪詢,然後期間通過比較key值,判斷是否返回新資料 - 客戶端第一次會帶一個空的key值,這次會立即返回,獲取新內容,伺服器端將計算出的contentKey返回給客戶端 - 然後客戶端傳送第二次請求,帶上第一次返回的contentKey作為key值,然後進行下一輪的比較 - 如果兩次的key值相同,就會hold請求,進行內部輪詢,如果期間有新內容或者客戶端timeout,就會斷開連線 - 重複以上步驟 ```javascript // 伺服器端 router.get('/v2/datas', function(req, res) { const key = _.get(req.query, 'key', ''); let contentKey = chatRoom.getContentKey(); while (key === contentKey) { sleep.sleep(5); contentKey = chatRoom.getContentKey(); } const connectors = chatRoom.getConnectors(); const messages = chatRoom.getMessages(); res.json({ code: 200, data: { connectors: connectors, messages: messages, key: contentKey }, }); }); ``` 以下是用 [express-longpoll](https://www.npmjs.com/package/express-longpoll) 的實現片段 ```javascript // mini-chatroom/public/javascripts/server/longPolling.js function pushDataToClient(key, longpoll) { var contentKey = chatRoom.getContentKey(); if (key !== contentKey) { var connectors = chatRoom.getConnectors(); var messages = chatRoom.getMessages(); longpoll.publish( '/v2/datas', { code: 200, data: {connectors: connectors, messages: messages, key: contentKey}, } ); } } longpoll.create("/v2/datas", function(req, res, next) { key = _.get(req.query, 'key', ''); pushDataToClient(key, longpoll); next(); }); intervalId = setInterval(function() { pushDataToClient(key, longpoll); }, LONG_POLLING_TIMEOUT); ``` 為了方便演示,我將客戶端發起請求的timeout改成了4s,注意觀察下面的截圖: ![longPollingNetwork](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016110956670-1296999533.gif) 可以看到,斷開連線的兩種方式,要麼是超時,要麼是請求有資料返回。 ### 基於iframe的長輪詢模式 這種模式的具體的原理為: - 在頁面中嵌入一個iframe,地址指向輪詢的伺服器地址,然後在父頁面中放置一個執行函式,比如`execute(data)` - 當伺服器有內容改變時,會向iframe傳送一個指令碼`` - 通過傳送的指令碼,主動執行父頁面中的方法,達到推送的效果 具體可以參看[這裡](https://juejin.im/post/6844903955240058893#heading-4) ## Websocket >The WebSocket Protocol enables two-way communication between a client running untrusted code in a controlled environment to a remote host that has opted-in to communications from that code. > >The protocol consists of an opening handshake followed by basic message framing, layered over TCP. > >The goal of this technology is to provide a mechanism for browser-based applications that need two-way communication with servers that does not rely on opening multiple HTTP connections (e.g., using XMLHttpRequest or iframe and long polling). > > >The WebSocket Protocol attempts to address the goals of existing bidirectional HTTP technologies in the context of the existing HTTP infrastructure; as such, it is designed to work over HTTP ports 80 and 443 as well as to support HTTP proxies and intermediaries, even if this implies some complexity specific to the current environment. > ### 特徵 - websocket是雙向通訊的,設計的目的主要是為了減少傳統輪詢時http連線數量的開銷 - 建立在TCP協議之上,握手階段採用 HTTP 協議,因此握手時不容易遮蔽,能通過各種 HTTP 代理伺服器 - 與HTTP相容性良好,同樣可以使用80和443埠 - 沒有同源限制,客戶端可以與任意伺服器通訊 - 可以傳送文字,也可以傳送二進位制資料。 - 協議識別符號是`ws`(如果加密,則為`wss`),伺服器網址就是 URL ![websocket](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111024184-1181679663.png) 關於Websocket API方面的知識,這裡不再作講解,可以自己查閱[Websocket API MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) ### 相容性 websocket相容性良好,基本支援所有現代瀏覽器 [![websocket1](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111051800-1589284363.png)](https://caniuse.com/mdn-api_websocket) ### 程式碼實現 筆者這裡採用的是[socket.io](https://socket.io/),是基於websocket的封裝,提供了客戶端以及伺服器端的支援 ```javascript // 客戶端 var WebsocketNotification = { // ... subscribe: function(args) { var connector = args[1]; this.socket = io(); this.socket.emit('register', connector); this.socket.on('register done', function() { window.ChatroomDOM.renderAfterRegister(); }); this.socket.on('data', function(res) { window.ChatroomDOM.renderData(res); }); this.socket.on('disconnect', function() { window.ChatroomDOM.renderAfterLogout(); }); } // ... } // 伺服器端 var io = socketIo(httpServer); io.on('connection', (socket) => { socket.on('register', function(connector) { chatRoom.onConnect(connector); io.emit('register done'); var data = chatRoom.getDatas(); io.emit('data', { data }); }); socket.on('chat', function(message) { chatRoom.receive(message); var data = chatRoom.getDatas(); io.emit('data', { data }); }); }); ``` 響應格式如下: ![websocket-request-response](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111114415-191156913.png) ## Server-Sent Events(SSE) 傳統意義上伺服器端不會主動推送給客戶端訊息,一般都是客戶端主動去請求伺服器端獲取最新的資料。[SSE](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events)就是一種可以主動從服務端推送訊息的技術。 > SSE的本質其實就是一個HTTP的長連線,只不過它給客戶端傳送的不是一次性的資料包,而是一個stream流,格式為text/event-stream,所以客戶端不會關閉連線,會一直等著伺服器發過來的新的資料流,視訊播放就是這樣的例子。 > > - SSE 使用 HTTP 協議,現有的伺服器軟體都支援。WebSocket 是一個獨立協議。 > - SSE 屬於輕量級,使用簡單;WebSocket 協議相對複雜。 > - SSE 預設支援斷線重連,WebSocket 需要自己實現。 > - SSE 一般只用來傳送文字,二進位制資料需要編碼後傳送,WebSocket 預設支援傳送二進位制資料。 > - SSE 支援自定義傳送的訊息型別。 基本的使用方法,參看[SSE API](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events) ![sse](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111137374-204514403.png) ### 相容性 目前除了IE以及低版本的瀏覽器不支援,基本支援絕大多數的現代瀏覽器。 [![sse2](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111243330-56262537.png)](https://caniuse.com/?search=Server-Sent%20Events) ### 程式碼實現 ```javascript // 客戶端 var SSENotification = { source: null, subscribe: function() { if ('EventSource' in window) { this.source = new EventSource('/sse'); this.source.addEventListener('message', function(res) { const d = res.data; window.ChatroomDOM.renderData(JSON.parse(d)); }); } return this.unsubscribe; }, unsubscribe: function () { this.source && this.source.close(); } } // 伺服器端 router.get('/sse', function(req, res) { const connectors = chatRoom.getConnectors(); const messages = chatRoom.getMessages(); const response = { code: 200, data: { connectors: connectors, messages: messages } }; res.writeHead(200, { "Content-Type":"text/event-stream", "Cache-Control":"no-cache", "Connection":"keep-alive", "Access-Control-Allow-Origin": '*', }); res.write("retry: 10000\n"); res.write("data: " + JSON.stringify(response) + "\n\n"); var unsubscribe = Event.subscribe(function() { const connectors = chatRoom.getConnectors(); const messages = chatRoom.getMessages(); const response = { code: 200, data: { connectors: connectors, messages: messages } }; res.write("data: " + JSON.stringify(response) + "\n\n"); }); req.connection.addListener("close", function () { unsubscribe(); }, false); }); ``` 下面是控制檯的情況,注意觀察響應型別 ![sse-type](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111315189-2009829249.png) 詳情中注意檢視請求型別,以及EventStream訊息型別 ![sse3](https://img2020.cnblogs.com/blog/681618/202010/681618-20201016111337249-2118037786.gif) ## 總結 - 短輪詢、長輪詢實現成本相對比較簡單,適用於一些實時性要求不高的訊息推送,在實時性要求高的場景下,會存在延遲以及會給伺服器帶來更大的壓力 - websocket目前而言實現成本相對較低,適合於雙工通訊,對於多人線上,要求實時性較高的專案比較實用 - SSE只能是伺服器端推送訊息,因此對於不需要雙向通訊的專案比較適用 ## 參考連線 - [The WebSocket Protocol](https://tools.ietf.org/html/rfc6455) - [Websocket API MDN](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket) - [Server-sent events MDN](https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events) - [WebSocket 教程](http://www.ruanyifeng.com/blog/2017/05/websocket.html) - [Server-Sent Events 教程](https://www.ruanyifeng.com/blog/2017/05/server-sent_events.html) - [webSocket(二) 短輪詢、長輪詢、Websocket、sse](https://juejin.im/post/6844903955240