Server-Sent Events 教程

阮一峰發表於2017-05-27

一、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

(完)

相關文章