跳至主要内容

Server Sent Event

什麼是 Server Sent Event

Server-Sent Events (SSE) 是一種由伺服器主動推送資料到客戶端的技術。與 WebSockets 不同,SSE 是單向的,即只允許伺服器向客戶端推送資料,而客戶端不能向伺服器發送資料。SSE 通常用於即時更新應用,例如新聞推送、社交媒體通知、即時股票價格等。

SSE 使用 HTTP 協議進行通信。具體過程如下:

  1. 客戶端發起一個長輪詢(long-polling)請求到伺服器。
  2. 伺服器保持這個連接,並在有新資料時推送到客戶端。
  3. 連接以 stream 的形式傳輸資料,資料格式為一行一條消息,消息之間用兩個換行符分隔。
  4. 這種機制保證了客戶端可以接收到伺服器即時推送的更新資料,而不需要反覆發起新的請求。

優缺點

優點缺點
簡單易用: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
  • 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")

前端實作

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.
}
},
});

參考資料