SSE ( Server-sent Events )是 WebSocket 的一種輕量代替方案,使用 HTTP 協議。
嚴格地說,HTTP 協議是沒有辦法做服務器推送的,但是當服務器向客戶端聲明接下來要發送流信息時,客戶端就會保持連接打開,SSE 使用的就是這種原理。
一、SSE 能做什么?
理論上, SSE 和 WebSocket 做的是同一件事情。當你需要用新數據局部更新網絡應用時,SSE 可以做到不需要用戶執行任何操作,便可以完成。
舉例我們要做一個統計系統的管理后台,我們想知道統計數據的實時情況。類似這種更新頻繁、 低延遲的場景,SSE 可以完全滿足。
其他一些應用場景:例如郵箱服務的新郵件提醒,微博的新消息推送、管理后台的一些操作實時同步等,SSE 都是不錯的選擇。
二、SSE vs. WebSocket
SSE 是單向通道,只能服務器向客戶端發送消息,如果客戶端需要向服務器發送消息,則需要一個新的 HTTP 請求。 這對比 WebSocket 的雙工通道來說,會有更大的開銷。這么一來的話就會存在一個「什么時候才需要關心這個差異?」的問題,如果平均每秒會向服務器發送一次消息的話,那應該選擇 WebSocket。如果一分鍾僅 5 - 6 次的話,其實這個差異並不大。
在瀏覽器兼容方面,兩者差不多。在較早之前,每當需要建立雙向 Socket 時就會使用 Flash,在 移動瀏覽器不支持 Flash 的情況下,WebSocket 的兼容是比較難做的。
SSE 我認為最大的優勢是便利:
- 實現一個完整的服務僅需要少量的代碼;
- 可以在現有的服務中使用,不需要啟動一個新的服務;
- 可以用任何一種服務端語言中使用;
- 基於 HTTP / HTTPS 協議,可以直接運行於現有的代理服務器和認證技術。
有了這些優勢,在選擇使用 SSE 時就已經為自己的項目節約了不少成本。
三、SSE(Server-sent Events)在HTML 5中的技術規范和定義
Server-sent Events 規范是 HTML 5 規范的一個組成部分,具體的規范文檔見參考資源。該規范比較簡單,主要由兩個部分組成:
第一個部分是服務器端與瀏覽器端之間的通訊協議,
第二部分則是在瀏覽器端可供 JavaScript 使用的 EventSource 對象。
通訊協議是基於純文本的簡單協議。服務器端的響應的內容類型是“text/event-stream”。響應文本的內容可以看成是一個事件流,由不同的事件所組成。
每個事件由類型和數據兩部分組成,同時每個事件可以有一個可選的標識符。不同事件的內容之間通過僅包含回車符和換行符的空行(“\r\n”)來分隔。每個事件的數據可能由多行組成。
下面代碼給出了服務器端響應的示例:
data: first event data: second event id: 100 event: myevent data: third event id: 101 : this is a comment data: fourth event data: fourth event continue
如上所示,每個事件之間通過空行來分隔。對於每一行來說,冒號(“:”)前面表示的是該行的類型,冒號后面則是對應的值。可能的類型包括:
- 類型為空白,表示該行是注釋,會在處理時被忽略。
- 類型為 data,表示該行包含的是數據。以 data 開頭的行可以出現多次。所有這些行都是該事件的數據。
- 類型為 event,表示該行用來聲明事件的類型。瀏覽器在收到數據時,會產生對應類型的事件。
- 類型為 id,表示該行用來聲明事件的標識符。
- 類型為 retry,表示該行用來聲明瀏覽器在連接斷開之后進行再次連接之前的等待時間。
在上述代碼中,第一個事件只包含數據“first event”,會產生默認的事件;第二個事件的標識符是 100,數據為“second event”;第三個事件會產生類型為“myevent”的事件;最后一個事件的數據為“fourth event\nfourth event continue”。當有多行數據時,實際的數據由每行數據以換行符連接而成。
如果服務器端返回的數據中包含了事件的標識符,瀏覽器會記錄最近一次接收到的事件的標識符。如果與服務器端的連接中斷,當瀏覽器端再次進行連接時,會通過 HTTP 頭“Last-Event-ID”來聲明最后一次接收到的事件的標識符。服務器端可以通過瀏覽器端發送的事件標識符來確定從哪個事件開始來繼續連接。
對於服務器端返回的響應,瀏覽器端需要在 JavaScript 中使用 EventSource 對象來進行處理。EventSource 使用的是標准的事件監聽器方式,只需要在對象上添加相應的事件處理方法即可。EventSource 提供了三個標准事件

如之前所述,服務器端可以返回自定義類型的事件。對於這些事件,可以使用 addEventListener 方法來添加相應的事件處理方法。如下代碼給出了 EventSource 對象的使用示例。
var es = new EventSource('events'); es.onmessage = function(e) { console.log(e.data); }; es.addEventListener('myevent', function(e) { console.log(e.data); });
在指定 URL 創建出 EventSource 對象之后,可以通過 onmessage 和 addEventListener 方法來添加事件處理方法。當服務器端有新的事件產生,相應的事件處理方法會被調用。EventSource 對象的 onmessage 屬性的作用類似於 addEventListener( ‘ message ’ ),不過 onmessage 屬性只支持一個事件處理方法。
四、簡單示例
下面是一個簡單的示例,實現一個 SSE 服務。
1、服務端
'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 個字符,但是發送到客戶端上的時候不會觸發任何事件,這對客戶端來說是非常友好的。所以注釋一般被用於維持服務器和客戶端的長連接。
效果:
2、客戶端
我們創建了一個 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 個方法:
EventHandler 屬性
EventSource.onopen 在連接打開時被調用。
EventSource.onmessage 在收到一個沒有 event 屬性的消息時被調用。
EventSource.onerror 在連接異常時被調用。
只讀屬性
EventSource.readyState 一個 unsigned short 值,代表連接狀態。可能值是 CONNECTING (0), OPEN (1), 或者 CLOSED (2)。
EventSource.url 連接的 URL。
方法
EventSource.close() 關閉連接
效果:
五、SSE使用注意事項
1、SSE 如何保證數據完整性
客戶端在每次接收到消息時,會把消息的 id 字段作為內部屬性 Last-Event-ID 儲存起來。
SSE 默認支持斷線重連機制,在連接斷開時會 觸發 EventSource 的 error 事件,同時自動重連。再次連接成功時 EventSource 會把 Last-Event-ID 屬性作為請求頭發送給服務器,這樣服務器就可以根據這個 Last-Event-ID 作出相應的處理。
這里需要注意的是,id 字段不是必須的,服務器有可能不會在消息中帶上 id 字段,這樣子客戶端就不會存在 Last-Event-ID 這個屬性。所以為了保證數據可靠,我們需要在每條消息上帶上 id 字段。
2、減少開銷
在 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();
3、瀏覽器兼容

向下兼容:早些時候,為了實現數據實時更新最常見的方法就是輪詢。
輪詢是以一個固定頻率向服務器發送請求,服務器在有 數據更新時 返回新的數據,以此來管理數據的更新。這種輪詢的方式不但開銷大,而且更新的效率和頻率有關,也不能達到及時更新的目的。
接着便出現了長輪詢的方式:客戶端向服務器發送請求之后,服務器會暫時把請求掛起,等到有數據更新時再返回最新的數據給客戶端,客戶端在接收到新的消息后再向服務器發送請求。與常規輪詢的不同之處是:數據可以做到實時更新,可以減少不必要的開銷。
這里有一個「選擇長輪詢還是常規輪詢?」的命題,長輪詢是不是總比常規輪詢占有優勢?我們可以從帶寬占用的角度分析,如果一個程序數據更新太過頻繁,假設每秒 2 次更新,如果使用長輪詢的話每分鍾要發送 120 次 HTTP 請求。如果使用常規輪詢,每 5 秒發送一次請求的話, 一分鍾才 20 次,從這里看,常規輪詢更占有優勢。
長輪詢和 SSE 最關鍵的區別在於,每一次數據更新都需要一次 HTTP 請求。和 WebSocket 還有 SSE 一樣,長輪詢也會 占用一個 socket。在數據更新效率上和 SSE 差不多,一有數據更新就能檢測到。加上所有瀏覽器都支持,是一個不錯的 SSE 替代方案。
文章介紹了 SSE 的用法及使用過程中的一些技巧。對比 WebSocket,SSE 在開發時間和成本上占有較大的優勢。做數據推送服務,除了 WebSocket,SSE 也是一個不錯的選擇,希望對大家有所幫助。
