SSE(Server-Sent Events):伺服器推送技術
SSE(Server-Sent Events,伺服器發送事件)是一種輕量級的伺服器推送技術。相比 WebSocket,它更簡單且基於 HTTP。
一、 什麼是 SSE?
1.1 概念
SSE 允許伺服器主動向客戶端推送資料:
1.2 特點
| 特點 | 說明 |
|---|---|
| 單向通訊 | 伺服器 → 客戶端 |
| 基於 HTTP | 不需要特殊協定 |
| 自動重連 | 斷線自動恢復 |
| 文字格式 | UTF-8 編碼 |
| 事件驅動 | 可定義事件類型 |
二、 HTTP 協定細節
2.1 請求
http
GET /events HTTP/1.1
Accept: text/event-stream
Cache-Control: no-cache2.2 回應
http
HTTP/1.1 200 OK
Content-Type: text/event-stream
Cache-Control: no-cache
Connection: keep-alive
data: Hello
data: {"user": "John", "message": "Hi"}
event: notification
data: You have a new message
id: 12345
event: update
data: {"price": 100}
retry: 50002.3 資料格式
| 欄位 | 說明 |
|---|---|
data: | 訊息內容(必須) |
event: | 事件類型(可選) |
id: | 事件 ID(用於重連) |
retry: | 重連間隔(毫秒) |
三、 後端實作
3.1 Express 基礎實作
javascript
app.get("/events", (req, res) => {
// 設定 SSE 標頭
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
res.setHeader("X-Accel-Buffering", "no"); // Nginx
// 發送訊息
res.write("data: Connected\n\n");
// 定時發送
const interval = setInterval(() => {
res.write(`data: ${new Date().toISOString()}\n\n`);
}, 1000);
// 客戶端斷線
req.on("close", () => {
clearInterval(interval);
res.end();
});
});3.2 發送不同類型事件
javascript
function sendEvent(res, event, data, id) {
let message = "";
if (id) {
message += `id: ${id}\n`;
}
if (event) {
message += `event: ${event}\n`;
}
message += `data: ${JSON.stringify(data)}\n\n`;
res.write(message);
}
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
// 連線確認
sendEvent(res, "connected", { status: "ok" });
// 模擬不同事件
let messageId = 0;
const interval = setInterval(() => {
messageId++;
if (messageId % 5 === 0) {
sendEvent(
res,
"notification",
{
title: "New alert",
body: "Something happened",
},
messageId
);
} else {
sendEvent(
res,
"message",
{
time: new Date().toISOString(),
},
messageId
);
}
}, 1000);
req.on("close", () => {
clearInterval(interval);
});
});3.3 廣播給多個客戶端
javascript
const clients = new Set();
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
res.setHeader("Cache-Control", "no-cache");
res.setHeader("Connection", "keep-alive");
clients.add(res);
res.write("data: Connected\n\n");
req.on("close", () => {
clients.delete(res);
});
});
// 廣播函數
function broadcast(event, data) {
const message = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
clients.forEach((client) => {
client.write(message);
});
}
// 使用
app.post("/notify", (req, res) => {
broadcast("notification", req.body);
res.json({ sent: clients.size });
});四、 前端實作
4.1 EventSource API
javascript
const eventSource = new EventSource("/events");
// 預設訊息事件
eventSource.onmessage = (event) => {
console.log("Message:", event.data);
};
// 自訂事件
eventSource.addEventListener("notification", (event) => {
const data = JSON.parse(event.data);
showNotification(data.title, data.body);
});
eventSource.addEventListener("update", (event) => {
const data = JSON.parse(event.data);
updateUI(data);
});
// 連線事件
eventSource.onopen = () => {
console.log("Connected");
};
// 錯誤處理
eventSource.onerror = (error) => {
console.error("SSE Error:", error);
if (eventSource.readyState === EventSource.CLOSED) {
console.log("Connection closed");
}
};
// 關閉連線
// eventSource.close()4.2 帶認證的 SSE
EventSource 不支援自訂標頭,需要使用其他方式:
javascript
// 方法 1:URL 參數
const eventSource = new EventSource(`/events?token=${token}`);
// 方法 2:Cookie(推薦)
// 確保 Cookie 設定正確
// 方法 3:使用 fetch + ReadableStream
async function sseWithHeaders(url, headers) {
const response = await fetch(url, {
headers: {
Authorization: `Bearer ${token}`,
...headers,
},
});
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) break;
const text = decoder.decode(value);
parseSSE(text);
}
}
function parseSSE(text) {
const lines = text.split("\n");
let event = "message";
let data = "";
lines.forEach((line) => {
if (line.startsWith("event:")) {
event = line.slice(6).trim();
} else if (line.startsWith("data:")) {
data = line.slice(5).trim();
} else if (line === "") {
if (data) {
onEvent(event, data);
event = "message";
data = "";
}
}
});
}五、 SSE vs WebSocket
5.1 比較表
| 特性 | SSE | WebSocket |
|---|---|---|
| 通訊方向 | 單向(伺服器 → 客戶端) | 雙向 |
| 協定 | HTTP | ws:// / wss:// |
| 瀏覽器支援 | 良好(IE 除外) | 優秀 |
| 自動重連 | ✅ 內建 | ❌ 需自己實作 |
| 資料格式 | 文字 | 文字 + 二進位 |
| 複雜度 | 簡單 | 較複雜 |
| 代理相容 | 良好 | 可能有問題 |
5.2 何時使用哪個?
使用 SSE 的場景:
- 即時通知
- 股票報價
- 新聞推送
- 進度更新
使用 WebSocket 的場景:
- 即時聊天
- 多人遊戲
- 協作編輯
- 需要客戶端發送訊息
六、 進階用法
6.1 重連與 Last-Event-ID
javascript
// 後端:發送帶 ID 的事件
res.write(`id: ${eventId}\ndata: ${data}\n\n`);
// 斷線重連時,瀏覽器會自動帶上 Last-Event-ID
app.get("/events", (req, res) => {
const lastEventId = req.headers["last-event-id"];
if (lastEventId) {
// 發送遺漏的事件
const missedEvents = getEventsSince(lastEventId);
missedEvents.forEach((event) => {
sendEvent(res, event.type, event.data, event.id);
});
}
// 繼續正常推送
// ...
});6.2 心跳保持連線
javascript
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
// 每 30 秒發送心跳
const heartbeat = setInterval(() => {
res.write(": heartbeat\n\n"); // 冒號開頭是註解
}, 30000);
req.on("close", () => {
clearInterval(heartbeat);
});
});6.3 與 Redis Pub/Sub 整合
javascript
const Redis = require("ioredis");
const subscriber = new Redis();
subscriber.subscribe("updates");
const clients = new Set();
subscriber.on("message", (channel, message) => {
const event = JSON.parse(message);
clients.forEach((client) => {
client.write(
`event: ${event.type}\ndata: ${JSON.stringify(event.data)}\n\n`
);
});
});
app.get("/events", (req, res) => {
res.setHeader("Content-Type", "text/event-stream");
clients.add(res);
req.on("close", () => {
clients.delete(res);
});
});總結
| 概念 | 說明 |
|---|---|
| SSE | 伺服器發送事件 |
| text/event-stream | MIME 類型 |
| EventSource | 前端 API |
| data/event/id | 訊息欄位 |
| 自動重連 | 內建機制 |
> **選擇建議**:
- 只需要伺服器推送 → SSE
- 需要雙向通訊 → WebSocket
- 需要最大兼容性 → WebSocket + Polyfill
進階挑戰
- 實作一個即時股票報價系統,使用 SSE 推送價格更新。
- 比較 SSE 和 WebSocket 在高併發下的資源消耗。
- 研究如何在 Nginx 後正確配置 SSE。