跳至主要內容
Skip to content

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-cache

2.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: 5000

2.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 比較表

特性SSEWebSocket
通訊方向單向(伺服器 → 客戶端)雙向
協定HTTPws:// / 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-streamMIME 類型
EventSource前端 API
data/event/id訊息欄位
自動重連內建機制

> **選擇建議**:

  • 只需要伺服器推送 → SSE
  • 需要雙向通訊 → WebSocket
  • 需要最大兼容性 → WebSocket + Polyfill

進階挑戰

  1. 實作一個即時股票報價系統,使用 SSE 推送價格更新。
  2. 比較 SSE 和 WebSocket 在高併發下的資源消耗。
  3. 研究如何在 Nginx 後正確配置 SSE。

延伸閱讀與資源