使用伺服器傳送的事件串流更新

伺服器傳送事件 (SSE) 會使用 HTTP 連線,從伺服器將自動更新傳送給用戶端。連線建立後,伺服器即可啟動資料傳輸程序。

您可以使用 SSE,從網頁應用程式傳送推播通知。SSE 會以一個方向傳送資訊,因此您將不會收到來自用戶端的更新。

SSE 的概念您可能熟悉,網頁應用程式會「訂閱」伺服器產生的更新的串流,而且每當發生新事件時,都會傳送通知給用戶端。但為了確實瞭解伺服器傳送事件,我們還需要瞭解其 AJAX 前項的限制。包括:

  • 輪詢:應用程式會重複輪詢伺服器來查詢資料。大部分的 AJAX 應用程式都使用這項技術。使用 HTTP 通訊協定時,擷取資料的方法與要求和回應格式有關。用戶端提出要求,並等待伺服器回應資料。如果沒有可用的資料,會傳回空白回應。額外的輪詢作業會帶來較大的 HTTP 負擔。

  • 長時間輪詢 (Hanging GET / COMET):如果伺服器沒有可用資料,伺服器會保留要求,直到有新資料可供使用為止。因此,這項技術通常稱為「Hanging GET」。有可用資訊時,伺服器會回應並關閉連線,然後重複程序。因此,伺服器會持續以新資料回應。如要進行這項設定,開發人員通常會利用入侵手段,例如將指令碼標記附加至「無限」 iframe。

伺服器傳送事件的設計從零開始設計到效率更高,與 SSE 通訊時,伺服器可以隨時將資料推送至應用程式,無需提出初始要求。換句話說,當更新發生時,可以從伺服器將更新串流至用戶端。SSE 會在伺服器和用戶端之間開啟單一單向通道。

伺服器傳送事件與長時間輪詢之間的主要差異在於,SSE 會直接由瀏覽器處理,而使用者只需聽取訊息即可。

伺服器傳送事件與 WebSocket

為什麼要選擇使用 WebSocket 的伺服器傳送事件?就讓我來回答您的問題!

WebSockets 具有豐富的通訊協定,可與雙向的全雙工通訊。雙向管道較適合用於遊戲、訊息應用程式,以及需要近距離即時更新的任何用途。

不過,有時您只需要來自伺服器的單向通訊即可。 例如,好友更新自己的狀態、股票代號、新聞動態饋給或其他自動化資料推送機制時。換句話說,用戶端網路 SQL 資料庫或 IndexedDB 物件儲存庫的更新。如果您需要將資料傳送至伺服器,XMLHttpRequest 一律會是好友。

SSE 會透過 HTTP 傳送。不需要特殊的通訊協定或伺服器實作就能運作。WebSocket 需要完整雙工連線和新的 WebSocket 伺服器才能處理通訊協定。

此外,伺服器傳送事件具有 WebSocket 設計所缺乏的多種功能,包括自動重新連線、事件 ID,以及傳送任意事件的功能。

使用 JavaScript 建立 EventSource

如要訂閱事件串流,請建立 EventSource 物件,並將串流的網址傳送給該物件:

const source = new EventSource('stream.php');

接下來,請為 message 事件設定處理常式。您可以選擇監聽 openerror

source.addEventListener('message', (e) => {
  console.log(e.data);
});

source.addEventListener('open', (e) => {
  // Connection was opened.
});

source.addEventListener('error', (e) => {
  if (e.readyState == EventSource.CLOSED) {
    // Connection was closed.
  }
});

從伺服器推送更新時,onmessage 處理常式會觸發,並在其 e.data 屬性中提供新資料。最神奇的部分是,每次連線關閉時,瀏覽器會在 3 秒後自動重新連線至來源。您的伺服器實作甚至可以控制這個重新連線逾時

就是這麼簡單!您的客戶現在可以處理來自「stream.php」的事件。

事件串流格式

從來源傳送事件串流,必須建構採用 SSE 格式的 text/event-stream Content-Type 的純文字回應。以基本格式來說,回應應包含 data: 行,後面接著訊息,然後加上兩個「\n」字元結束串流:

data: My message\n\n

多行資料

如果訊息較長,則可以使用多行 data: 行拆分。系統會將以 data: 開頭的兩行以上連續資料視為單一資料,這表示只會觸發一個 message 事件。

每一行都應以單一「\n」結尾 (最後一個行結尾應為兩個)。傳送至 message 處理常式的結果是由換行字元串連的單一字串。例如:

data: first line\n
data: second line\n\n</pre>

這會在 e.data 中產生「第一行\n 第二行」。這樣一來,您就可以使用 e.data.split('\n').join('') 來重新建構訊息 San Sant「\n」字元。

傳送 JSON 資料

使用多行程式碼可協助您在不破壞語法的情況下傳送 JSON:

data: {\n
data: "msg": "hello world",\n
data: "id": 12345\n
data: }\n\n

可能的用戶端程式碼來處理該串流:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.id, data.msg);
});

將 ID 與事件建立關聯

您可以加入開頭為 id: 的一行程式碼,利用串流事件傳送專屬 ID:

id: 12345\n
data: GOOG\n
data: 556\n\n

設定 ID 可讓瀏覽器追蹤上一個事件觸發,一旦伺服器的連線中斷,系統就會透過新的要求設定特殊的 HTTP 標頭 (Last-Event-ID)。以便瀏覽器判斷最適合觸發的事件。 message 事件包含 e.lastEventId 屬性。

控管重新連線逾時

每次連線關閉後,瀏覽器大約都會嘗試重新連線至來源。如要變更逾時設定,請加入以 retry: 開頭的行,接著加入嘗試重新重新連線前要等待的毫秒數。

下列範例會在 10 秒後嘗試重新連線:

retry: 10000\n
data: hello world\n\n

指定事件名稱

單一事件來源可以加入事件名稱,來產生不同類型的事件。如果存在以 event: 開頭的行,後面接著事件的專屬名稱,該事件會與該名稱相關聯。在用戶端上,您可以設定事件監聽器來監聽該特定事件。

舉例來說,以下伺服器輸出內容會傳送三種事件類型:一般的「message」事件、「userlogon」和「update」事件:

data: {"msg": "First message"}\n\n
event: userlogon\n
data: {"username": "John123"}\n\n
event: update\n
data: {"username": "John123", "emotion": "happy"}\n\n

在用戶端上設定事件監聽器時:

source.addEventListener('message', (e) => {
  const data = JSON.parse(e.data);
  console.log(data.msg);
});

source.addEventListener('userlogon', (e) => {
  const data = JSON.parse(e.data);
  console.log(`User login: ${data.username}`);
});

source.addEventListener('update', (e) => {
  const data = JSON.parse(e.data);
  console.log(`${data.username} is now ${data.emotion}`);
};

伺服器範例

以下是在 PHP 中實作的基本伺服器實作:

<?php
header('Content-Type: text/event-stream');
header('Cache-Control: no-cache'); // recommended to prevent caching of event data.

/**
* Constructs the SSE data format and flushes that data to the client.
*
* @param string $id Timestamp/id of this connection.
* @param string $msg Line of text that should be transmitted.
**/

function sendMsg($id, $msg) {
  echo "id: $id" . PHP_EOL;
  echo "data: $msg" . PHP_EOL;
  echo PHP_EOL;
  ob_flush();
  flush();
}

$serverTime = time();

sendMsg($serverTime, 'server time: ' . date("h:i:s", time()));
?>

以下使用 Express 處理常式在 Node JS 上進行類似的實作:

app.get('/events', (req, res) => {
    // Send the SSE header.
    res.writeHead(200, {
        'Content-Type': 'text/event-stream',
        'Cache-Control': 'no-cache',
        'Connection': 'keep-alive'
    });

    // Sends an event to the client where the data is the current date,
    // then schedules the event to happen again after 5 seconds.
    const sendEvent = () => {
        const data = (new Date()).toLocaleTimeString();
        res.write("data: " + data + '\n\n');
        setTimeout(sendEvent, 5000);
    };

    // Send the initial event immediately.
    sendEvent();
});

sse-node.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <script>
    const source = new EventSource('/events');
    source.onmessage = (e) => {
        const content = document.createElement('div');
        content.textContent = e.data;
        document.body.append(content);
    };
    </script>
  </body>
</html>

取消事件串流

一般而言,瀏覽器會在連線關閉時自動重新連線至事件來源,但用戶端或伺服器可以取消該行為。

如要從用戶端取消串流,請呼叫:

source.close();

如要從伺服器取消串流,請使用非 text/event-stream Content-Type 回應,或傳回 200 OK 以外的 HTTP 狀態 (例如 404 Not Found)。

這兩種方法都會阻止瀏覽器重新建立連線。

安全性宣言

EventSource 產生的要求必須遵守與其他網路 API (例如擷取) 相同的來源政策。如需可從不同來源存取伺服器上的 SSE 端點,請參閱如何使用跨源資源共享 (CORS) 來啟用。