一、SSE 的本質
嚴格地說,HTTP 協議無法做到伺服器主動推送資訊。但是,有一種變通方法,就是伺服器向客戶端宣告,接下來要傳送的是流資訊(streaming)。
也就是說,傳送的不是一次性的資料包,而是一個資料流,會連續不斷地傳送過來。這時,客戶端不會關閉連線,會一直等著伺服器發過來的新的資料流,影片播放就是這樣的例子。本質上,這種通訊就是以流資訊的方式,完成一次用時很長的下載。
SSE 就是利用這種機制,使用流資訊向瀏覽器推送資訊。它基於 HTTP 協議,目前除了 IE/Edge,其他瀏覽器都支援。
二、SSE 的特點
SSE 與 WebSocket 作用相似,都是建立瀏覽器與伺服器之間的通訊渠道,然後伺服器向瀏覽器推送資訊。
總體來說,WebSocket 更強大和靈活。因為它是全雙工通道,可以雙向通訊;SSE 是單向通道,只能伺服器向瀏覽器傳送,因為流資訊本質上就是下載。如果瀏覽器向伺服器傳送資訊,就變成了另一次 HTTP 請求。
但是,SSE 也有自己的優點。
- SSE 使用 HTTP 協議,現有的伺服器軟體都支援。WebSocket 是一個獨立協議。
- SSE 屬於輕量級,使用簡單;WebSocket 協議相對複雜。
- SSE 預設支援斷線重連,WebSocket 需要自己實現。
- SSE 一般只用來傳送文字,二進位制資料需要編碼後傳送,WebSocket 預設支援傳送二進位制資料。
- SSE 支援自定義傳送的訊息型別。
因此,兩者各有特點,適合不同的場合。
三、客戶端 API
3.1 EventSource 物件
SSE 的客戶端 API 部署在EventSource
物件上。下面的程式碼可以檢測瀏覽器是否支援 SSE。
if ('EventSource' in window) { // ... }
使用 SSE 時,瀏覽器首先生成一個EventSource
例項,向伺服器發起連線。
var source = new EventSource(url);
上面的url
可以與當前網址同域,也可以跨域。跨域時,可以指定第二個引數,開啟withCredentials
屬性,表示是否一起傳送 Cookie。
var source = new EventSource(url, { withCredentials: true });
EventSource
例項的readyState
屬性,表明連線的當前狀態。該屬性只讀,可以取以下值。
- 0:相當於常量
EventSource.CONNECTING
,表示連線還未建立,或者斷線正在重連。- 1:相當於常量
EventSource.OPEN
,表示連線已經建立,可以接受資料。- 2:相當於常量
EventSource.CLOSED
,表示連線已斷,且不會重連。
3.2 基本用法
連線一旦建立,就會觸發open
事件,可以在onopen
屬性定義回撥函式。
source.onopen = function (event) { // ... }; // 另一種寫法 source.addEventListener('open', function (event) { // ... }, false);
客戶端收到伺服器發來的資料,就會觸發message
事件,可以在onmessage
屬性的回撥函式。
source.onmessage = function (event) { var data = event.data; // handle message }; // 另一種寫法 source.addEventListener('message', function (event) { var data = event.data; // handle message }, false);
上面程式碼中,事件物件的data
屬性就是伺服器端傳回的資料(文字格式)。
如果發生通訊錯誤(比如連線中斷),就會觸發error
事件,可以在onerror
屬性定義回撥函式。
source.onerror = function (event) { // handle error event }; // 另一種寫法 source.addEventListener('error', function (event) { // handle error event }, false);
close
方法用於關閉 SSE 連線。
source.close();
3.3 自定義事件
預設情況下,伺服器發來的資料,總是觸發瀏覽器EventSource
例項的message
事件。開發者還可以自定義 SSE 事件,這種情況下,傳送回來的資料不會觸發message
事件。
source.addEventListener('foo', function (event) { var data = event.data; // handle message }, false);
上面程式碼中,瀏覽器對 SSE 的foo
事件進行監聽。如何實現伺服器傳送foo
事件,請看下文。
四、伺服器實現
4.1 資料格式
伺服器向瀏覽器傳送的 SSE 資料,必須是 UTF-8 編碼的文字,具有如下的 HTTP 頭資訊。
Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive
上面三行之中,第一行的Content-Type
必須指定 MIME 型別為event-steam
。
每一次傳送的資訊,由若干個message
組成,每個message
之間用\n\n
分隔。每個message
內部由若干行組成,每一行都是如下格式。
[field]: value\n
上面的field
可以取四個值。
- data
- event
- id
- retry
此外,還可以有冒號開頭的行,表示註釋。通常,伺服器每隔一段時間就會向瀏覽器傳送一個註釋,保持連線不中斷。
: This is a comment
下面是一個例子。
: this is a test stream\n\n data: some text\n\n data: another message\n data: with two lines \n\n
4.2 data 欄位
資料內容用data
欄位表示。
data: message\n\n
如果資料很長,可以分成多行,最後一行用\n\n
結尾,前面行都用\n
結尾。
data: begin message\n data: continue message\n\n
下面是一個傳送 JSON 資料的例子。
data: {\n data: "foo": "bar",\n data: "baz", 555\n data: }\n\n
4.3 id 欄位
資料識別符號用id
欄位表示,相當於每一條資料的編號。
id: msg1\n data: message\n\n
瀏覽器用lastEventId
屬性讀取這個值。一旦連線斷線,瀏覽器會傳送一個 HTTP 頭,裡面包含一個特殊的Last-Event-ID
頭資訊,將這個值傳送回來,用來幫助伺服器端重建連線。因此,這個頭資訊可以被視為一種同步機制。
4.4 event 欄位
event
欄位表示自定義的事件型別,預設是message
事件。瀏覽器可以用addEventListener()
監聽該事件。
event: foo\n data: a foo event\n\n data: an unnamed event\n\n event: bar\n data: a bar event\n\n
上面的程式碼創造了三條資訊。第一條的名字是foo
,觸發瀏覽器的foo
事件;第二條未取名,表示預設型別,觸發瀏覽器的message
事件;第三條是bar
,觸發瀏覽器的bar
事件。
下面是另一個例子。
event: userconnect data: {"username": "bobby", "time": "02:33:48"} event: usermessage data: {"username": "bobby", "time": "02:34:11", "text": "Hi everyone."} event: userdisconnect data: {"username": "bobby", "time": "02:34:23"} event: usermessage data: {"username": "sean", "time": "02:34:36", "text": "Bye, bobby."}
4.5 retry 欄位
伺服器可以用retry
欄位,指定瀏覽器重新發起連線的時間間隔。
retry: 10000\n
兩種情況會導致瀏覽器重新發起連線:一種是時間間隔到期,二是由於網路錯誤等原因,導致連線出錯。
五、Node 伺服器例項
SSE 要求伺服器與瀏覽器保持連線。對於不同的伺服器軟體來說,所消耗的資源是不一樣的。Apache 伺服器,每個連線就是一個執行緒,如果要維持大量連線,勢必要消耗大量資源。Node 則是所有連線都使用同一個執行緒,因此消耗的資源會小得多,但是這要求每個連線不能包含很耗時的操作,比如磁碟的 IO 讀寫。
下面是 Node 的 SSE 伺服器例項。
var http = require("http"); http.createServer(function (req, res) { var fileName = "." + req.url; if (fileName === "./stream") { 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("event: connecttime\n"); res.write("data: " + (new Date()) + "\n\n"); res.write("data: " + (new Date()) + "\n\n"); interval = setInterval(function () { res.write("data: " + (new Date()) + "\n\n"); }, 1000); req.connection.addListener("close", function () { clearInterval(interval); }, false); } }).listen(8844, "127.0.0.1");
請將上面的程式碼儲存為server.js
,然後執行下面的命令。
$ node server.js
上面的命令會在本機的8844
埠,開啟一個 HTTP 服務。
然後,開啟這個網頁,檢視客戶端程式碼並執行。
六、參考連結
- Colin Ihrig, Implementing Push Technology Using Server-Sent Events
- Colin Ihrig,The Server Side of Server-Sent Events
- Eric Bidelman, Stream Updates with Server-Sent Events
- MDN,Using server-sent events
- Segment.io, Server-Sent Events: The simplest realtime browser spec
(完)