Server Sent Event
什麼是 Server Sent Event
Server-Sent Events (SSE) 是一種由伺服器主動推送資料到客戶端的技術。與 WebSockets 不同,SSE 是單向的,即只允許伺服器向客戶端推送資料,而客戶端不能向伺服器發送資料。SSE 通常用於即時更新應用,例如新聞推送、社交媒體通知、即時股票價格等。
SSE 使用 HTTP 協議進行通信。具體過程如下:
- 客戶端發起一個長輪詢(long-polling)請求到伺服器。
- 伺服器保持這個連接,並在有新資料時推送到客戶端。
- 連接以 stream 的形式傳輸資料,資料格式為一行一條消息,消息之間用兩個換行符分隔。
- 這種機制保證了客戶端可以接收到伺服器即時推送的更新資料,而不需要反覆發起新的請求。
優缺點
優點 | 缺點 |
---|---|
簡單易用:SSE 使用簡單的 HTTP 協議,瀏覽器原生支持,無需額外的 library | 單向通信:SSE 僅支持從伺服器到客戶端的單向資料傳輸 |
自動重連:瀏覽器會自動處理 SSE 連接的斷開和重連,不需要額外的邏輯 | 連接限制:部分瀏覽器對同一源的並發連接數有限制 |
文本傳輸:SSE 使用純文本傳輸資料,簡單直觀,便於調試 | 不支持二進制:SSE 僅支持文本資料傳輸,不適合傳輸二進制資料 |
有序傳輸:消息按照發送順序接收,保證資料的有序性 | 受限於 HTTP/1.1:在 HTTP/1.1 上性能可能不如 HTTP/2.0 |
廣泛支持:被大多數現代瀏覽器支持,兼容性好 | 防火牆和代理:某些防火牆和代理可能阻止長時間的 HTTP 連接 |
低資源消耗:相比 polling ,SSE 連接更持久,減少了伺服器資源消耗 | 伺服器負擔:長時間保持連接,伺服器需要處理更多的持久連接 |
後端實作
透過建立具備以下 Header 的回應
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
- FastAPI
- id 字段可自定義事件 ID
- event 字段可自定義事件名稱
- retry 字段可自定義重連時間
- 必須確保回傳資訊使用 data: 字段
- 字段之間使用 \n 字段
- 最後一行使用 \n\n 作為結尾
from fastapi import FastAPI
from fastapi.responses import StreamingResponse
import time
app = FastAPI()
def event_stream():
pointer = 0
id = 123
retry = 15000
while pointer < 3:
# 失敗事件
yield f"id:{id}\nretry:{retry}\nevent: error\ndata: system has some error.\n\n"
pointer += 1
time.sleep(1)
while pointer < 5:
# 成功事件
yield f"id:{id}\nretry:{retry}\ndata: The time is {time.strftime('%Y-%m-%d %H:%M:%S')}\n\n"
pointer += 1
time.sleep(1)
else:
# 結束事件
yield "id:{id}\nretry:{retry}\nevent: end\ndata: The end\n\n"
@app.get("/sse")
async def sse():
return StreamingResponse(event_stream(), media_type="text/event-stream")
前端實作
- EventSource
- Fetch Event Source
- 僅能接收 GET 請求
- 無法帶入 header 和 body 兩種資訊
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>SSE Example</title>
</head>
<body>
<h1>Server-Sent Events Example</h1>
<div id="messages"></div>
<script>
if ("EventSource" in window) {
const eventSource = new EventSource("http://localhost:8000/sse");
// 原生事件
eventSource.onmessage = function (event) {
const newElement = document.createElement("div");
newElement.textContent = event.data;
document.getElementById("messages").appendChild(newElement);
};
// 原生結束事件
eventSource.onerror = function (event) {
console.error("EventSource failed:", event);
eventSource.close();
};
// 客製化結束事件
eventSource.addEventListener("end", function (event) {
console.log("EventSource end:", event);
eventSource.close();
});
}
</script>
</body>
</html>
- 透過 fetch-event-source 套件實作
const ctrl = new AbortController();
class RetriableError extends Error {}
class FatalError extends Error {}
fetchEventSource("/sse", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
foo: "bar",
}),
signal: ctrl.signal,
async onopen(response) {
if (
response.ok &&
response.headers.get("content-type") === EventStreamContentType
) {
return; // everything's good
} else if (
response.status >= 400 &&
response.status < 500 &&
response.status !== 429
) {
// client-side errors are usually non-retriable:
throw new FatalError();
} else {
throw new RetriableError();
}
},
onmessage(msg) {
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
if (msg.event === "FatalError") {
throw new FatalError(msg.data);
}
},
onclose() {
// if the server closes the connection unexpectedly, retry:
throw new RetriableError();
},
onerror(err) {
if (err instanceof FatalError) {
throw err; // rethrow to stop the operation
} else {
// do nothing to automatically retry. You can also
// return a specific retry interval here.
}
},
});