Server-Sent Events 即時通訊實戰指南
5 分鐘閱讀 900 字
Server-Sent Events 即時通訊實戰指南
在現代 Web 開發中,即時通訊已成為許多應用的標配。從股票行情、社群媒體動態到 CI/CD 日誌串流,使用者期望不重新整理頁面就能看到最新資料。雖然 WebSocket 是雙向即時通訊的首選,但有一種更輕量的方案往往被工程師忽略——Server-Sent Events(SSE)。
什麼是 Server-Sent Events?
SSE 是 HTML5 標準的一部分,允許伺服器透過 HTTP 連線主動向客戶端推送資料。與 WebSocket 不同,SSE 是單向的:只有伺服器可以發送訊息給客戶端,客戶端無法透過同一連線回傳資料。
這個限制聽起來像缺點,但在許多場景下反而是優勢:
- 基於標準 HTTP,不需要特殊的協定升級
- 自動重連機制內建於瀏覽器
- 支援事件 ID 和斷線重連後的訊息補送
- 防火牆和代理伺服器通常不會阻擋 HTTP 連線
SSE 的訊息格式
SSE 使用純文字格式,每個訊息由空行分隔:
data: 這是一則訊息
data: 這是第二則訊息
id: 42
event: custom-event
data: {"user": "alice", "message": "Hello"}
id: 43欄位說明:
data:訊息內容,可以跨多行id:事件 ID,用於斷線重連後告知伺服器最後收到的訊息event:自訂事件名稱,預設為messageretry:重連延遲時間(毫秒)
伺服器端實作(Node.js + Express)
const express = require('express');
const app = express();
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.setHeader('Access-Control-Allow-Origin', '*');
res.write('data: 連線成功\n\n');
const interval = setInterval(() => {
const now = new Date().toISOString();
res.write(`data: ${JSON.stringify({ time: now })}\n\n`);
}, 1000);
req.on('close', () => {
clearInterval(interval);
console.log('Client disconnected');
});
});
app.listen(3000);客戶端實作(原生 JavaScript)
const eventSource = new EventSource('/events');
eventSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
document.getElementById('output').textContent = data.time;
});
eventSource.addEventListener('user-joined', (event) => {
const user = JSON.parse(event.data);
console.log(`${user.name} 加入了聊天室`);
});
eventSource.addEventListener('error', (event) => {
if (eventSource.readyState === EventSource.CLOSED) {
console.log('連線已關閉');
} else {
console.error('連線錯誤,正在重試...');
}
});斷線重連與事件 ID
實際生產環境中需要處理客戶端斷線後的訊息補送:
app.get('/events', (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const lastEventId = req.headers['last-event-id'];
if (lastEventId) {
const missedMessages = getMissedMessages(parseInt(lastEventId));
missedMessages.forEach(msg => {
res.write(`id: ${msg.id}\n`);
res.write(`data: ${JSON.stringify(msg)}\n\n`);
});
}
let messageId = parseInt(lastEventId || '0');
const interval = setInterval(() => {
messageId++;
res.write(`id: ${messageId}\n`);
res.write(`data: ${JSON.stringify({ id: messageId, ts: Date.now() })}\n\n`);
}, 2000);
req.on('close', () => clearInterval(interval));
});在 Vue 3 中整合 SSE
<script setup>
import { ref, onMounted, onUnmounted } from 'vue';
const messages = ref([]);
const isConnected = ref(false);
let eventSource = null;
onMounted(() => {
eventSource = new EventSource('/api/events');
eventSource.addEventListener('open', () => {
isConnected.value = true;
});
eventSource.addEventListener('message', (e) => {
const data = JSON.parse(e.data);
messages.value.unshift(data);
if (messages.value.length > 50) messages.value.pop();
});
eventSource.addEventListener('error', () => {
isConnected.value = false;
});
});
onUnmounted(() => eventSource?.close());
</script>
<template>
<div>
<span :class="isConnected ? 'badge-green' : 'badge-red'">
{{ isConnected ? '即時連線中' : '連線中斷' }}
</span>
<ul>
<li v-for="msg in messages" :key="msg.id">{{ msg.content }}</li>
</ul>
</div>
</template>SSE vs WebSocket 比較
| 特性 | SSE | WebSocket |
|---|---|---|
| 通訊方向 | 單向(伺服器→客戶端) | 雙向 |
| 底層協定 | HTTP | WS/WSS |
| 自動重連 | 瀏覽器內建 | 需手動實作 |
| 負載均衡 | 容易(無狀態) | 需要 sticky session |
| 適合場景 | 通知、動態更新 | 聊天、遊戲 |
Nginx 代理設定
在 Nginx 後面使用 SSE 時,需關閉緩衝:
location /events {
proxy_pass http://backend;
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection keep-alive;
proxy_read_timeout 86400s;
}實際應用場景
- 即時通知系統:新訂單、新評論推送
- CI/CD 日誌串流:即時顯示建置日誌
- 加密貨幣行情:持續更新價格
- 儀表板數據:伺服器定期推送統計數據
- 任務進度回報:長時間任務的進度條更新
小結
SSE 是一個被低估的技術。對於需要伺服器推送但不需要雙向通訊的場景,SSE 比 WebSocket 更簡單、更易維護。下次遇到即時推送需求時,不妨先評估 SSE 是否已足夠,再決定是否引入 WebSocket 的複雜性。
分享這篇文章