跳至主要內容

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:自訂事件名稱,預設為 message
  • retry:重連延遲時間(毫秒)

伺服器端實作(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;
}

實際應用場景

  1. 即時通知系統:新訂單、新評論推送
  2. CI/CD 日誌串流:即時顯示建置日誌
  3. 加密貨幣行情:持續更新價格
  4. 儀表板數據:伺服器定期推送統計數據
  5. 任務進度回報:長時間任務的進度條更新

小結

SSE 是一個被低估的技術。對於需要伺服器推送但不需要雙向通訊的場景,SSE 比 WebSocket 更簡單、更易維護。下次遇到即時推送需求時,不妨先評估 SSE 是否已足夠,再決定是否引入 WebSocket 的複雜性。

分享這篇文章