因爲 HTTP/1.1 自己不支持服務器主動向客戶端推送消息,在例如即時通信、消息提醒等應用場景中就會很不方便。解決的方法有不少,WebSocket 就很不錯,可是若是想要快速實現的話就不推薦使用,本文要介紹的是一種輕量的解決方案:SSE。javascript
SSE 是基於 HTTP 協議來完成服務器推送的。不要誤會,並非說 HTTP/2,而是一種取巧的方式:當服務器向客戶端聲明接下來要發送流信息時,客戶端就會保持鏈接打開,SSE 使用的就是這種原理。java
理論上,SSE 和 WebSocket 作的是同一件事情。當你須要用新數據局部更新網絡應用時,SSE 能夠作到不須要用戶執行任何操做,即可以完成。瀏覽器
舉例咱們要作一個統計系統的管理後臺,咱們想知道統計數據的實時狀況。相似這種更新頻繁、低延遲的場景,SSE 能夠徹底知足。服務器
其餘一些應用場景:例如郵箱服務的新郵件提醒,微博的新消息推送、管理後臺的一些操做實時同步等,SSE 都是不錯的選擇。網絡
SSE 是單向通道,只能服務器向客戶端發送消息,若是客戶端須要向服務器發送消息,則須要一個新的 HTTP 請求。這對比 WebSocket 的雙工通道來講,會有更大的開銷。這麼一來的話就會存在一個「何時才須要關心這個差別?」的問題,若是平均每秒會向服務器發送一次消息的話,那應該選擇 WebSocket。若是一分鐘僅 5 - 6 次的話,其實這個差別並不大。socket
在瀏覽器兼容方面,二者差很少。在較早以前,每當須要創建雙向 Socket 時就會使用 Flash,在移動瀏覽器不支持 Flash 的狀況下,WebSocket 的兼容是比較難作的。ide
SSE 我認爲最大的優點是便利:ui
有了這些優點,在選擇使用 SSE 時就已經爲本身的項目節約了很多成本。編碼
下面是一個簡單的示例,實現一個 SSE 服務。url
'use strict'; const http = require('http'); http.createServer((req, res) => { // 服務器聲明接下來發送的是事件流 res.writeHead(200, { 'Content-Type': 'text/event-stream', 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'Access-Control-Allow-Origin': '*', }); // 發送消息 setInterval(() => { res.write('event: slide\n'); // 事件類型 res.write(`id: ${+new Date()}\n`); // 消息ID res.write('data: 7\n'); // 消息數據 res.write('retry: 10000\n'); // 重連時間 res.write('\n\n'); // 消息結束 }, 3000); // 發送註釋保持長鏈接 setInterval(() => { res.write(': \n\n'); }, 12000); }).listen(2000);
服務器首先向客戶端聲明接下來發送的是事件流(text/event-stream)類型的數據,而後就能夠向客戶端屢次發送消息。
事件流是一個簡單的文本流,僅支持 UTF-8 格式的編碼。每條消息以一個空行做爲分隔符。
在規範中爲消息定義了 4 個字段:
event 消息的事件類型。客戶端收到消息時,會在當前的 EventSource 對象上觸發一個事件,這個事件的名稱就是這個字段的值,若是消息沒有這個字段,客戶端的 EventSource 對象就會觸發默認的 message 事件。
id 這條消息的 ID。客戶端接收到消息後,會把這個 ID 做爲內部屬性 Last-Event-ID
,在斷開重連成功後,會把 Last-Event-ID
發送給服務器。
data 消息的數據字段。客戶端會把這個字段解析爲字符串,若是一條消息有多個 data 字段,客戶端會自動用換行符鏈接成一個字符串。
retry 指定客戶端重連的時間。只接受整數,單位是毫秒。若是這個值不是整數則會被自動忽略。
一個頗有意思的地方是,規範中規定以冒號開頭的消息都會被看成註釋,一條普通的註釋(:\n\n
)對於服務器來講只佔 5 個字符,可是發送到客戶端上的時候不會觸發任何事件,這對客戶端來講是很是友好的。因此註釋通常被用於維持服務器和客戶端的長鏈接。
咱們建立了一個 EventSource
對象,傳入參數:url
。而且根據服務器的狀態和發送的信息做出響應。
'use strict'; if (window.EventSource) { // 建立 EventSource 對象鏈接服務器 const source = new EventSource('http://localhost:2000'); // 鏈接成功後會觸發open事件 source.addEventListener('open', () => { console.log('Connected'); }, false); // 服務器發送信息到客戶端時,若是沒有event字段,默認會觸發message事件 source.addEventListener('message', e => { console.log(`data: ${e.data}`); }, false); // 自定義EventHandler,在收到event字段爲slide的消息時觸發 source.addEventListener('slide', e => { console.log(`data: ${e.data}`); // => data: 7 }, false); // 鏈接異常時會觸發error事件並自動重連 source.addEventListener('error', e => { if (e.target.readyState === EventSource.CLOSED) { console.log('Disconnected'); } else if (e.target.readyState === EventSource.CONNECTING) { console.log('Connecting...'); } }, false); } else { console.error('Your browser doesn\'t support SSE'); }
EventSource從父接口 EventTarget 中繼承了屬性和方法,其內置了 3 個 EventHandler 屬性、2 個只讀屬性和 1 個方法:
EventSource.onopen 在鏈接打開時被調用。
EventSource.onmessage 在收到一個沒有 event 屬性的消息時被調用。
EventSource.onerror 在鏈接異常時被調用。
EventSource.readyState 一個 unsigned short 值,表明鏈接狀態。可能值是CONNECTING (0), OPEN (1), 或者 CLOSED (2)。
EventSource.url 鏈接的 URL。
EventSource.close() 關閉鏈接。
客戶端在每次接收到消息時,會把消息的 id 字段做爲內部屬性 Last-Event-ID
儲存起來。
SSE 默認支持斷線重連機制,在鏈接斷開時會觸發EventSource 的 error 事件,同時自動重連。再次鏈接成功時 EventSource 會把 Last-Event-ID
屬性做爲請求頭髮送給服務器,這樣服務器就能夠根據這個 Last-Event-ID
做出相應的處理。
這裏須要注意的是,id 字段不是必須的,服務器有可能不會在消息中帶上 id 字段,這樣子客戶端就不會存在 Last-Event-ID
這個屬性。因此爲了保證數據可靠,咱們須要在每條消息上帶上 id 字段。
在 SSE 的草案中提到,"text/event-stream" 的 MIME 類型傳輸應當在靜置 15 秒後自動斷開。在實際的項目中也會有這個機制,可是斷開的時間沒有被列入標準中。
爲了減小服務器的開銷,咱們也能夠有目的的斷開和重連。
簡單的辦法是服務器發送一個關閉消息並指定一個重連的時間戳,客戶端在觸發關閉事件時關閉當前鏈接並建立一個計時器,在重連時把計時器銷燬。
'use strict'; function connectSSE() { if (window.EventSource) { const source = new EventSource('http://localhost:2000'); let reconnectTimeout; source.addEventListener('open', () => { console.log('Connected'); clearTimeout(reconnectTimeout); }, false); source.addEventListener('pause', e => { source.close(); const reconnectTime = +e.data; const currentTime = +new Date(); reconnectTimeout = setTimeout(() => { connectSSE(); }, reconnectTime - currentTime); }, false); } else { console.error('Your browser doesn\'t support SSE'); } } connectSSE();
Broswer support of EventSource from Can I Use...
早些時候,爲了實現數據實時更新最多見的方法就是輪詢。
輪詢是以一個固定頻率向服務器發送請求,服務器在有數據更新時返回新的數據,以此來管理數據的更新。這種輪詢的方式不但開銷大,並且更新的效率和頻率有關,也不能達到及時更新的目的。
接着便出現了長輪詢的方式:客戶端向服務器發送請求以後,服務器會暫時把請求掛起,等到有數據更新時再返回最新的數據給客戶端,客戶端在接收到新的消息後再向服務器發送請求。與常規輪詢的不一樣之處是:數據能夠作到實時更新,能夠減小沒必要要的開銷。
這裏有一個「選擇長輪詢仍是常規輪詢?」的命題,長輪詢是否是總比常規輪詢佔有優點?咱們能夠從帶寬佔用的角度分析,若是一個程序數據更新太過頻繁,假設每秒 2 次更新,若是使用長輪詢的話每分鐘要發送 120 次 HTTP 請求。若是使用常規輪詢,每 5 秒發送一次請求的話,一分鐘才 20 次,從這裏看,常規輪詢更佔有優點。
長輪詢和 SSE 最關鍵的區別在於,每一次數據更新都須要一次 HTTP 請求。和 WebSocket 還有 SSE 同樣,長輪詢也會佔用一個 socket。在數據更新效率上和 SSE 差很少,一有數據更新就能檢測到。加上全部瀏覽器都支持,是一個不錯的 SSE 替代方案。
文章介紹了 SSE 的用法及使用過程當中的一些技巧。對比 WebSocket,SSE 在開發時間和成本上佔有較大的優點。作數據推送服務,除了 WebSocket,SSE 也是一個不錯的選擇,但願對你們有所幫助。
Server-Sent Events
EventSource - Web APIs | MDN
Using server-sent events - Web APIs | MDN
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證) 轉載請註明出處
原文地址:20 行代碼寫一個數據推送服務
文章做者:何啓邦
版權聲明:自由轉載-非商用-非衍生-保持署名(創意共享3.0許可證) 轉載請註明出處