伺服器向瀏覽器推送資訊,除了 WebSocket,還有一種方法:Server-Sent Events(以下簡稱 SSE)。本文介紹它的用法。
一、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。
1 2 3 |
if ('EventSource' in window) { // ... } |
使用 SSE 時,瀏覽器首先生成一個EventSource例項,向伺服器發起連線。
1 |
var source = new EventSource(url); |
上面的url可以與當前網址同域,也可以跨域。跨域時,可以指定第二個引數,開啟withCredentials屬性,表示是否一起傳送 Cookie。
1 |
var source = new EventSource(url, { withCredentials: true }); |
EventSource例項的readyState屬性,表明連線的當前狀態。該屬性只讀,可以取以下值。
- 0:相當於常量EventSource.CONNECTING,表示連線還未建立,或者斷線正在重連。
- 1:相當於常量EventSource.OPEN,表示連線已經建立,可以接受資料。
- 2:相當於常量EventSource.CLOSED,表示連線已斷,且不會重連。
3.2 基本用法
連線一旦建立,就會觸發open事件,可以在onopen屬性定義回撥函式。
1 2 3 4 5 6 7 8 |
source.onopen = function (event) { // ... }; // 另一種寫法 source.addEventListener('open', function (event) { // ... }, false); |
客戶端收到伺服器發來的資料,就會觸發message事件,可以在onmessage屬性的回撥函式。
1 2 3 4 5 6 7 8 9 10 |
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屬性定義回撥函式。
1 2 3 4 5 6 7 8 |
source.onerror = function (event) { // handle error event }; // 另一種寫法 source.addEventListener('error', function (event) { // handle error event }, false); |
close方法用於關閉 SSE 連線。
1 |
source.close(); |
3.3 自定義事件
預設情況下,伺服器發來的資料,總是觸發瀏覽器EventSource例項的message事件。開發者還可以自定義 SSE 事件,這種情況下,傳送回來的資料不會觸發message事件。
1 2 3 4 |
source.addEventListener('foo', function (event) { var data = event.data; // handle message }, false); |
上面程式碼中,瀏覽器對 SSE 的foo事件進行監聽。如何實現伺服器傳送foo事件,請看下文。
四、伺服器實現
4.1 資料格式
伺服器向瀏覽器傳送的 SSE 資料,必須是 UTF-8 編碼的文字,具有如下的 HTTP 頭資訊。
1 2 3 |
Content-Type: text/event-stream Cache-Control: no-cache Connection: keep-alive |
上面三行之中,第一行的Content-Type必須指定 MIME 型別為event-steam。
每一次傳送的資訊,由若干個message組成,每個message之間用nn分隔。每個message內部由若干行組成,每一行都是如下格式。
1 |
[field]: valuen |
上面的field可以取四個值。
- data
- event
- id
- retry
此外,還可以有冒號開頭的行,表示註釋。通常,伺服器每隔一段時間就會向瀏覽器傳送一個註釋,保持連線不中斷。
1 |
: This is a comment |
下面是一個例子。
1 2 3 4 5 6 |
: this is a test streamnn data: some textnn data: another messagen data: with two lines nn |
4.2 data 欄位
資料內容用data欄位表示。
1 |
data: messagenn |
如果資料很長,可以分成多行,最後一行用nn結尾,前面行都用n結尾。
1 2 |
data: begin messagen data: continue messagenn |
下面是一個傳送 JSON 資料的例子。
1 2 3 4 |
data: {n data: "foo": "bar",n data: "baz", 555n data: }nn |
4.3 id 欄位
資料識別符號用id欄位表示,相當於每一條資料的編號。
1 2 |
id: msg1n data: messagenn |
瀏覽器用lastEventId屬性讀取這個值。一旦連線斷線,瀏覽器會傳送一個 HTTP 頭,裡面包含一個特殊的Last-Event-ID頭資訊,將這個值傳送回來,用來幫助伺服器端重建連線。因此,這個頭資訊可以被視為一種同步機制。
4.4 event 欄位
event欄位表示自定義的事件型別,預設是message事件。瀏覽器可以用addEventListener()監聽該事件。
1 2 3 4 5 6 7 |
event: foon data: a foo eventnn data: an unnamed eventnn event: barn data: a bar eventnn |
上面的程式碼創造了三條資訊。第一條的名字是foo,觸發瀏覽器的foo事件;第二條未取名,表示預設型別,觸發瀏覽器的message事件;第三條是bar,觸發瀏覽器的bar事件。
下面是另一個例子。
1 2 3 4 5 6 7 8 9 10 11 |
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欄位,指定瀏覽器重新發起連線的時間間隔。
1 |
retry: 10000n |
兩種情況會導致瀏覽器重新發起連線:一種是時間間隔到期,二是由於網路錯誤等原因,導致連線出錯。
五、Node 伺服器例項
SSE 要求伺服器與瀏覽器保持連線。對於不同的伺服器軟體來說,所消耗的資源是不一樣的。Apache 伺服器,每個連線就是一個執行緒,如果要維持大量連線,勢必要消耗大量資源。Node 則是所有連線都使用同一個執行緒,因此消耗的資源會小得多,但是這要求每個連線不能包含很耗時的操作,比如磁碟的 IO 讀寫。
下面是 Node 的 SSE 伺服器例項。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
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: 10000n"); res.write("event: connecttimen"); res.write("data: " + (new Date()) + "nn"); res.write("data: " + (new Date()) + "nn"); interval = setInterval(function () { res.write("data: " + (new Date()) + "nn"); }, 1000); req.connection.addListener("close", function () { clearInterval(interval); }, false); } }).listen(8844, "127.0.0.1"); |
請將上面的程式碼儲存為server.js,然後執行下面的命令。
1 |
$ 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
(完)