伺服器推送事件 (SSE) 會透過 HTTP 連線,從伺服器自動傳送更新至用戶端。連線建立後,伺服器就能啟動資料傳輸。
您可能會想使用 SSE 從網路應用程式傳送推播通知。SSE 會單向傳送資訊,因此您不會收到用戶端的更新。
您可能對 SSE 的概念不陌生。網頁應用程式會「訂閱」伺服器產生的更新串流,並在發生新事件時傳送通知給用戶端。不過,要真正瞭解伺服器傳送事件,我們必須瞭解其前身 AJAX 的限制。包括:
輪詢:應用程式會重複輪詢伺服器以取得資料。大多數 AJAX 應用程式都會使用這項技巧。使用 HTTP 通訊協定時,擷取資料會圍繞要求和回應格式進行。用戶端會提出要求,並等待伺服器傳回資料。如果沒有可用的資料,系統會傳回空白回應。額外的輪詢會產生更大的 HTTP 額外負擔。
長時間輪詢 (掛起 GET / COMET):如果伺服器沒有可用的資料,伺服器會將要求保持開啟狀態,直到有新資料可用為止。因此,這項技巧通常稱為「掛起 GET」。當資訊可供使用時,伺服器會回應並關閉連線,然後重複這個程序。因此,伺服器會持續以新資料回應。為此,開發人員通常會使用駭客攻擊,例如在「無限」iframe 中附加指令碼標記。
伺服器傳送的事件是從一開始就以高效率為設計目標。與 SSE 通訊時,伺服器可隨時將資料推送至應用程式,而無需提出初始要求。換句話說,更新內容會在發生時從伺服器串流至用戶端。SSE 會在伺服器和用戶端之間開啟單一單向管道。
伺服器傳送事件和長時間輪詢的主要差異在於,SSE 會直接由瀏覽器處理,而使用者只需監聽訊息即可。
伺服器傳送的事件與 WebSocket
為何要選擇伺服器傳送事件,而非 WebSocket?就讓我來回答您的問題!
WebSockets 提供豐富的通訊協定,可進行雙向全雙工通訊。雙向通道更適合遊戲、訊息應用程式,以及任何需要雙向即時更新的用途。
不過,有時候您只需要伺服器的單向通訊。例如,當好友更新狀態、股票代碼、新聞動態或其他自動推送資料機制時。也就是說,這是用於更新用戶端 Web SQL 資料庫或 IndexedDB 物件儲存庫的更新。如果您需要將資料傳送至伺服器,XMLHttpRequest
一律會是您的好幫手。
SSE 會透過 HTTP 傳送。沒有任何特殊通訊協定或伺服器實作方式可用來運作。WebSocket 需要全雙工連線和新的 WebSocket 伺服器來處理通訊協定。
此外,伺服器傳送的事件具有 WebSocket 缺少的各種功能,包括自動重新連線、事件 ID 和傳送任意事件的功能。
使用 JavaScript 建立 EventSource
如要訂閱事件串流,請建立 EventSource
物件,並傳遞串流的網址:
const source = new EventSource('stream.php');
接下來,請為 message
事件設定處理常式。您可以選擇監聽 open
和 error
:
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」結尾 (最後一行除外,該行應以兩個「\n」結尾)。傳遞至 message
處理常式的結果,是透過換行字元連接的單一字串。例如:
data: first line\n
data: second line\n\n</pre>
這會在 e.data
中產生「first line\nsecond line」。接著,您可以使用 e.data.split('\n').join('')
重建不含「\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
屬性。
控管重新連線逾時時間
瀏覽器會在每個連線關閉後大約 3 秒,嘗試重新連線至來源。您可以變更逾時時間,方法是加入以 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 產生的要求,會受到與 fetch 等其他網路 API 相同的來源政策規範。如果您需要從不同來源存取伺服器上的 SSE 端點,請參閱如何啟用跨來源資源共享 (CORS)。