SSE(服務端發送事件)介紹

SSE(服務端發送事件)介紹

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 服務。

服務器

'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 個方法:

EventHandler 屬性

EventSource.onopen 在連接打開時被調用。

EventSource.onmessage 在收到一個沒有 event 屬性的消息時被調用。

EventSource.onerror 在連接異常時被調用。

只讀屬性

EventSource.readyState 一個 unsigned short 值,代表連接狀態。可能值是 CONNECTING (0), OPEN (1), 或者 CLOSED (2)。

EventSource.url 連接的 URL。

方法

EventSource.close() 關閉連接。

SSE 如何保證數據完整性

客戶端在每次接收到消息時,會把消息的 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();

瀏覽器兼容

SSE(服務端發送事件)介紹

向下兼容

早些時候,為了實現數據實時更新最常見的方法就是輪詢。

輪詢是以一個固定頻率向服務器發送請求,服務器在有 數據更新時 返回新的數據,以此來管理數據的更新。這種輪詢的方式不但開銷大,而且更新的效率和頻率有關,也不能達到及時更新的目的。

接著便出現了長輪詢的方式:客戶端向服務器發送請求之後,服務器會暫時把請求掛起,等到有數據更新時再返回最新的數據給客戶端,客戶端在接收到新的消息後再向服務器發送請求。與常規輪詢的不同之處是:數據可以做到實時更新,可以減少不必要的開銷。

這裡有一個「選擇長輪詢還是常規輪詢?」的命題,長輪詢是不是總比常規輪詢佔有優勢?我們可以從帶寬佔用的角度分析,如果一個程序數據更新太過頻繁,假設每秒 2 次更新,如果使用長輪詢的話每分鐘要發送 120 次 HTTP 請求。如果使用常規輪詢,每 5 秒發送一次請求的話, 一分鐘才 20 次,從這裡看,常規輪詢更佔有優勢。

長輪詢和 SSE 最關鍵的區別在於,每一次數據更新都需要一次 HTTP 請求。和 WebSocket 還有 SSE 一樣,長輪詢也會 佔用一個 socket。在數據更新效率上和 SSE 差不多,一有數據更新就能檢測到。加上所有瀏覽器都支持,是一個不錯的 SSE 替代方案。

結尾

文章介紹了 SSE 的用法及使用過程中的一些技巧。對比 WebSocket,SSE 在開發時間和成本上佔有較大的優勢。做數據推送服務,除了 WebSocket,SSE 也是一個不錯的選擇,希望對大家有所幫助。

補充

雙向通信與消息推送

輪詢:客戶端定時向服務器發送Ajax請求,服務器接到請求後馬上返回響應信息並關閉連接。 優點:後端程序編寫比較容易。 缺點:請求中有大半是無用,浪費帶寬和服務器資源。 實例:適於小型應用。

長輪詢:客戶端向服務器發送Ajax請求,服務器接到請求後hold住連接,直到有新消息才返回響應信息並關閉連接,客戶端處理完響應信息後再向服務器發送新的請求。 優點:在無消息的情況下不會頻繁的請求,耗費資小。 缺點:服務器hold連接會消耗資源,返回數據順序無保證,難於管理維護。 Comet異步的ashx,實例:WebQQ、Hi網頁版、Facebook IM。

長連接:在頁面裡嵌入一個隱蔵iframe,將這個隱蔵iframe的src屬性設為對一個長連接的請求或是採用xhr請求,服務器端就能源源不斷地往客戶端輸入數據。 優點:消息即時到達,不發無用請求;管理起來也相對便。 缺點:服務器維護一個長連接會增加開銷。 實例:Gmail聊天

Flash Socket:在頁面中內嵌入一個使用了Socket類的 Flash 程序JavaScript通過調用此Flash程序提供的Socket接口與服務器端的Socket接口進行通信,JavaScript在收到服務器端傳送的信息後控制頁面的顯示。 優點:實現真正的即時通信,而不是偽即時。 缺點:客戶端必須安裝Flash插件;非HTTP協議,無法自動穿越防火牆。 實例:網絡互動遊戲。

Websocket:

WebSocket是HTML5開始提供的一種瀏覽器與服務器間進行全雙工通訊的網絡技術。依靠這種技術可以實現客戶端和服務器端的長連接,雙向實時通信。

特點:

事件驅動、異步、使用ws或者wss協議的客戶端socket、能夠實現真正意義上的推送功能

缺點:少部分瀏覽器不支持,瀏覽器支持的程度與方式有區別。


分享到:


相關文章: