這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 這個問題? 這個問題一般會出現在面試題裡面,然後回答一些諸如輪詢、WebSocket之類的答案。當然,實際開發中,也會遇到類似別人給你贊了,要通知給你的情況。這時服務端推送給Web前端(先局限在Web前端,畢竟其他端還有一些特殊方法)到底 ...
這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助
這個問題?
這個問題一般會出現在面試題裡面,然後回答一些諸如輪詢、WebSocket之類的答案。當然,實際開發中,也會遇到類似別人給你贊了,要通知給你的情況。這時服務端推送給Web前端(先局限在Web前端,畢竟其他端還有一些特殊方法)到底有多少種方法?它們到底是怎麼實現的?
寫個Demo看看吧,這樣正好把主要(不清楚是否還有漏的)的方案都實現一遍。先看效果:
其中的代碼也上傳到GitHub
了,在server-push( github.com/waiter/serv… )這裡。
各種方案
從上面的截圖也已經可以看出,本文主要寫了5種方案,那麼接下來也就一個一個簡單介紹一下吧。
另外,本文涉及的Demo,後端直接使用原生的Node.js
開發,沒有使用Koa
、Express
之類的,也沒有使用額外的庫,類似socket.io
,主要是想保持最精簡的狀態來呈現。前端也只是在最基礎的HTML上,引入了jQuery
來方便做DOM操作,也引入了Bootstrap
來快速實現統一的樣式,而未再引入類似Vue
、React
之類的框架。
還有,為了觸發服務端推送,這邊在前端頁面上加了個輸入框和按鈕,來將消息發送給後端,後端會緩存消息,並觸發推送,後端大體代碼類似:
// 緩存需要推送的信息 const datas = []; // 各種方案觸發推送時的回調 const callbacks = {}; // 註冊介面回調 server.on('request', (req, res) => { const { pathname, query } = parse(req.url, true); // 如果發現是前端觸發推送介面 if (pathname === '/api/push') { if (query.info) { // 緩存推送信息 datas.push(query.info); const d = JSON.stringify([query.info]); // 觸發所有推送回調 Object.keys(callbacks).forEach(k => callbacks[k](d)); } res.end('ok'); } });
1. 輪詢(短輪詢)
這是最簡單直觀的方法,就是每隔一段時間發起一個請求到後端詢問是否有新信息。至於為什麼又叫短輪詢,其是相對於後續要說的長輪詢來對比的。
這樣前端只要設置一個setTimeout
來定時請求就行:
// 緩存前端已經獲取的最新id let id = 0; function poll() { $.ajax({ url: '/api/polling', data: { id }, }).done(res => { id += res.length; }).always(() => { // 10s後再次請求 setTimeout(poll, 10000); }); } poll();
後端也是否簡單,根據前端給到的id
,看看有沒有新消息,有就返回,沒有就返回空
const id = parseInt(query.id || '0', 10) || 0; res.writeHead(200, { 'Content-Type': 'application/json;' }); res.end(JSON.stringify(datas.slice(id)));
這個看起來其實時性與請求頻率成正相關,但是當請求頻率上來了,性能浪費也就越高,畢竟可能大部分請求都是無意義的。
2. 長輪詢
在翻找資料的時候,發現有些資料會直接把這個當作短輪詢
,有點匪夷所思。這裡的長輪詢相對前面的輪詢來說,算是一種優化。具體就是前端發起請求到後端,後端不直接返回,而是等待有新信息時再返回。所以這樣發起的一個請求,可能需要很長的時間才能等到返回,故而叫做長輪詢。
其前端代碼基本和短輪詢一致,只不過把請求的超時時間設置較長(比如1分鐘),然後無論請求成功或失敗,馬上再次發起請求即可。
相對來說,後端的寫法就要稍微改動一下
const id = parseInt(query.id || '0', 10) || 0; const cbk = 'long-polling'; delete callbacks[cbk]; const data = datas.slice(id); res.writeHead(200, { 'Content-Type': 'application/json' }); // 發起請求時,正好有新消息就返回 if (data.length) { return res.end(JSON.stringify(data)); } req.on('close', () => { delete callbacks[cbk]; }); // 註冊新消息回調 callbacks[cbk] = (d) => { res.end(d); };
這樣,**相對於短輪詢,少了很多無意義的請求,而且消息的實時性也非常好。**不過,當服務端有異常時,會導致長輪詢短時間內不斷發起請求,可能讓服務端承受更大的壓力,所以兩次長輪詢之間最好有一定間隔,或者異常檢測機制。
3. SSE(Server-sent events)
Traditionally, a web page has to send a request to the server to receive new data; that is, the page requests data from the server. With server-sent events, it's possible for a server to send new data to a web page at any time, by pushing messages to the web page. These incoming messages can be treated as Events + data inside the web page.
前面提到的輪詢、長輪詢都是一問一答式的,一次請求,無法推送多次消息到前端。而SSE就厲害了,一次請求,N次推送。
其原理,或者說類比,個人認為可以理解為下載一個巨大的文件,文件的內容分塊傳給前端,每塊就是一次消息推送。
聽起來很厲害,先看看後端代碼要怎麼寫
const cbk = 'sse'; delete callbacks[cbk]; res.writeHead(200, { // 這個是核心 'Content-Type': 'text/event-stream', 'Connection': 'keep-alive', }); // 把緩存的信息推送給前端 res.write(`data: ${JSON.stringify(datas)}\n\n`); // 註冊新消息回調 callbacks[cbk] = (d) => { res.write(`data: ${d}\n\n`); }; req.on('close', () => { delete callbacks[cbk]; });
後端代碼很簡單,核心在於Content-Type: text/event-stream
,這要讓前端知道這是SSE,還有就是傳輸信息的格式比較特別一點,詳細的可以看 MDN( developer.mozilla.org/en-US/docs/… )
而前端有專門的EventSource
來接收,使用起來很方便
const es = new EventSource('/api/sse'); es.onmessage = (e) => { try { const c = JSON.parse(e.data); } catch (err) { console.log(err); } }
這樣就好了,如果你打開Chrome
的開發者工具中的網路標簽,你就會發現Chrome
對於SSE請求,有專門的展示標簽
另外,**SSE還支持自動重連!**伺服器短時間異常,恢復之後,無需額外代碼,SSE就自動重連上了。不過,本人在實際工作中卻沒有碰到過SSE,也就在面試題中見過。
4. WebSocket
既然有了SSE,那還要WebSocket幹啥啊?因為WebSocket可以一次連接,雙向推送,而SSE只能從服務端推送到前端。從這個角度來看,用WebSocket來單做服務端推送,有點大材小用了。
另外,初見WebSocket,可能會對其與Socket的聯繫有點疑惑。Socket協議是與HTTP協議平級的,而WebSocket協議是基於HTTP協議的,不過兩者在使用層面上是十分相近的。
其前端使用寫法與SSE類似,十分簡單,只不過請求鏈接為ws://
或者wss://
開頭(相當於http://
和https://
)
const ws = new WebSocket('ws://localhost:3000/ws'); ws.onmessage = e => { try { const c = JSON.parse(e.data); } catch (err) { console.log(err); } };
而如果要用原生Node.js
來寫WebSocket服務,就會麻煩一些了,一般情況都會使用類似socket.io
之類的三方庫來降低實現成本。這邊也就在網上摘抄了一段代碼來簡單實現一下,詳細的可以看Github
上的Demo代碼
server.on('upgrade', (req, socket) => { const cbk = 'ws'; delete callbacks[cbk]; const acceptKey = req.headers['sec-websocket-key']; const hash = generateAcceptValue(acceptKey); const responseHeaders = [ 'HTTP/1.1 101 Web Socket Protocol Handshake', 'Upgrade: WebSocket', 'Connection: Upgrade', `Sec-WebSocket-Accept: ${hash}` ]; // 告知前端這是WebSocket協議 socket.write(responseHeaders.join('\r\n') + '\r\n\r\n'); // 發送數據 socket.write(constructReply(datas)); callbacks[cbk] = (d) => { socket.write(constructReply(d)); } socket.on('close', () => { delete callbacks[cbk]; }); });
這個在Chrome
瀏覽器中,也有專門的標簽頁展示
不過,它沒有像SSE一樣有自動重連,這塊需要自行實現。
一般網頁實時聊天之類需要雙向推送的,都會使用WebSocket來實現。
5. iFrame
這算是找資料的時候意外發現的,之前並不知道還有這樣的玩法。原理類似使用iFrame載入一個巨大的網頁,利用瀏覽器會一邊載入一邊解析執行返回的HTML,通過分次返回Script標簽來實現消息推送。其實現類似SSE,不過看起來就比較==hack==。
前端代碼很簡單,只不過要註冊一個回調給iframe使用
// 註冊給iframe使用的方法 window.change = function(data) { }; $('body').append('<iframe src="/api/iframe"></iframe>');
而後端也很簡單,有消息的時候返回script
標簽即可
const cbk = 'iframe'; delete callbacks[cbk]; // 返回緩存信息 res.write(`<script>window.parent.change(${JSON.stringify(datas)});</script>`); callbacks[cbk] = (d) => { res.write(`<script>window.parent.change(${d});</script>`); }; req.on('close', () => { delete callbacks[cbk]; });
相當奇淫巧技了。不過,似乎沒找到怎麼判斷載入異常的情況,可能需要自行加心跳來實現了。
另外,很多文章在說使用iFrame方法時,會導致瀏覽器顯示未載入完,圖標一直轉的樣子。但是個人認為,圖標一直轉是因為頁面一直沒有onload
,那麼在頁面onload
之後,再創建iFrame就應該沒有這個問題了。
總結一下
上面實現了5種推送的方案,弄了一個表格簡單對比一下
方案 | (準)實時 | 單次連接 | 自動重連 | 斷線檢測 | 雙向推送 | 無跨域 |
---|---|---|---|---|---|---|
短輪詢 | ❌ | ❌ | ➖ | ✅ | ❌ | ❌ |
長輪詢 | ✅ | ❌ | ➖ | ✅ | ❌ | ❌ |
SSE | ✅ | ✅ | ✅ | ✅ | ❌ | ❌ |
WebSocket | ✅ | ✅ | ❌ | ✅ | ✅ | ✅ |
iFrame | ✅ | ✅ | ❌ | ❌ | ❌ | ❌ |