← Back to list

初探 SSE(Server-Sent Events)

Hannah Wang · 2026-06-22 09:21 · 10 claps · 15.6 min read
#server-sent-events #https
Open on Medium ↗

初探 SSE(Server-Sent Events)

🔖 文章索引* 🔖

  1. SSE (Server-Sent Events) 介紹 2. 適合與不適合使用 SSE 的情境 3. *SSE 的侷限
  2. 建立 SSE 連線 4.1 伺服器端的幾個關鍵 Header 4.2 資料傳輸格式
  3. long polling, short polling, SSE, websocket 之間有什麼差異
  4. 參考資料

最近有個專案要從 webSocket 換到 SSE (Server-Sent Events),後端原本用的webSocket 套件是 pusher。此專案是公司的出入口安全系統,同仁早上到公司時,都要在大門先刷識別證,系統就會顯示此員工的部門、職稱、卡機位置、刷卡時間等資訊給警衛看,由於只有需要後端推播給前端,不需要來回溝通,所以 SSE 其實就夠了,同時也輕量許多。

SSE (Server-Sent Events) 介紹

如果把 websocket 比喻成電話,那 SSE 就像是廣播,是一種單向傳輸,讓伺服器主動推送資料到瀏覽器。SSE 的本質是一個持久化的 HTTP 連線,連線會保持開啟狀態,伺服器會不斷地往這個連線裡寫入新的數據,與一般的 HTTP 請求傳完就斷不同。

有以下特點:

  1. 單向的主動推送
  2. 基於 HTTP 協議
  3. 前端用 Web API EventSource 接收
  4. 如果遇到網路斷線、服務重啟、proxy 中斷等連線斷掉的情境,瀏覽器的 EventSource 會帶上 Last-Event-ID嘗試重新連線,Server 也可以指定斷線後多久要進行重連
  5. 支援不同事件類型 Type

  1. 比 webSocket 輕量

適合與不適合使用 SSE 的情境

SSE 適合單向傳輸的場景(sever to client),例如:通知中心、股票即時狀態等。不適合需要大量雙向互動的場景,例如多人遊戲、即時協作應用、聊天室等,這使用 webScoket 更合適

bytebytego

bytebytego

SSE 的侷限

以下舉幾個實務上常見的坑

  1. 連線數量限制

HTTP/1.1 下,瀏覽器對同一個網域的 TCP 連線數通常是 6 個, 如果是使用 HTTP/2HTTP/3 協定就不會有這問題,這叫做多路復用 (Multiplexing)

什麼算是一個連線數?

如果你在同一個瀏覽器打開了 6 個分頁,每個分頁都連到 example.com 接收 SSE 資料,就佔用了 6 個連線這時如果開了第 7 個分頁,那這個分頁的請求會進入 Pending 狀態,直到前面有連線的分頁被關閉

  1. 原生 EventSource 加 custom header 很不方便

另外一個侷限是原生的 EventSource 不方便加 custom header,WHATWG 定義的建構子是 new EventSource(url, { withCredentials }) ,沒有像 fetch 可以直接設定 headers 的參數

fetch('/api/user', {
  headers: {
    Authorization: `Bearer ${token}`,
  },
});
// The EventSource interface

interface EventSource : EventTarget {
  constructor(USVString url, optional EventSourceInit eventSourceInitDict = {});

  readonly attribute USVString url;
  readonly attribute boolean withCredentials;

  // ready state
  const unsigned short CONNECTING = 0;
  const unsigned short OPEN = 1;
  const unsigned short CLOSED = 2;
  readonly attribute unsigned short readyState;

  // networking
  attribute EventHandler onopen;
  attribute EventHandler onmessage;
  attribute EventHandler onerror;
  undefined close();
};

dictionary EventSourceInit {
  boolean withCredentials = false;
};

實務上若要驗證,通常用 cookie、session 或 polyfill

polyfill 在這的意義

用第三方套件模擬原生 EventSource,但底層可能用 fetch 或 XHR,讓其可以支援 custom headers

以下是一些套件整理,大致可以分成兩類:

  1. 接近 EventSource API 的 polyfill 或 replacement

  2. 不是原生 polyfill,但用 fetch 重新實作 SSE client,通常是為了可以 custom headers / POST / retry 控制

3. 反向代理可能會把資料先暫存起來(buffer)

怎麼判定現在資料被 buffer?

判定方式是可以在 Chrome DevTools 的 Network 裡點 SSE 那個 request,接著點 Response Tab。如果資料不是每隔一段時間一筆筆出現,而是延遲後一次出現好幾筆,通常是 buffer 問題。

例如 nginx 如果把回應先暫存起來,前端就不會即時收到資料了,常見解法是請後端在 header 加上 X-Accel-Buffering: no來關閉 buffering,並定期送 comment heartbeat ,透過定期送空訊號,讓長連線不要被當成閒置連線斷掉,同時也幫 server 更快發現 client 已經離線。如同上圖的 : heartbeat,這種以 : 開頭的是 SSE comment,不會觸發 onmessage

// Express

app.get('/api/events', (req, res) => {
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');
  res.setHeader('X-Accel-Buffering', 'no'); // X-Accel-Buffering

  res.flushHeaders?.();

  // 假設每 15 秒送一次 heartbeat
  const heartbeat = setInterval(() => {
    res.write(`: heartbeat\n\n`);
  }, 15000);

  const sendData = (data: unknown) => {
    res.write(`data: ${JSON.stringify(data)}\n\n`);
  };

  req.on('close', () => {
    clearInterval(heartbeat);
  });
});

⚠️ 在 MDN 的範例中不是使用 : heartbeat comment,而是定時送出 event: ping。這種寫法會被前端監聽到,但因為它會定期讓 SSE 有資料流動,所以也能達到類似 heartbeat 的效果 (完整範例 code)

echo "event: ping\n",
'data: {"time": "' . $curDate . '"}', "\n\n";

建立 SSE 連線

前面提到 SSE 是由 Client Side 發起一個 HTTP request,Server Side 保持這個 response 不結束,持續透過同一個連線把資料推送給 Client Side

伺服器端的幾個關鍵 Header

建立 SSE endpoint 時,伺服器端需要設定幾個關鍵的 Header

  • Content-Type: text/event-stream(告訴瀏覽器這是一個持續的 Data stream)
  • Cache-Control: no-cache(防止瀏覽器快取,確保即時性)
  • Connection: keep-alive(保持連線不中斷)

資料傳輸格式

SSE 傳回的是純文字,每一個事件區塊結尾都以兩次換行符號 (\n\n) 結尾。有幾個特定的欄位:

  • data: 訊息內容
  • event: 自定義事件名稱。前端可以根據不同的事件執行不同的邏輯
  • id: 訊息的唯一識別。如果連線中斷,瀏覽器重連時會發送 Last-Event-ID 給伺服器,伺服器就能補發漏掉的訊息
  • retry: 瀏覽器在斷線後,隔多久嘗試重新連線(單位:毫秒)
id: 100             // 唯一識別
event: flight       // 事件名稱
data: {"flight":[{...}]} // 訊息內容
// 前端可以用 flight 接收事件

function createSSEConnection(...) {
let eventSource = null;
...

eventSource.addEventListener("flight", (event) => {
  console.log(data.percent);
  ...
});
}

以下附上一份較完整的範例 code:

type SSEOptions<T = unknown> = {
  url: string;
  withCredentials?: boolean;
  onOpen?: (event: Event) => void; // 連線成功時觸發
  onMessage?: (data: T, event: MessageEvent) => void; // 瀏覽器收到完整事件後觸發 
  onError?: (event: Event) => void; // 連線錯誤或中斷時觸發
  events?: Record<string, (data: T, event: MessageEvent) => void>;
  parseJson?: boolean;
};

export function createSSE<T = unknown>({
  url,
  withCredentials = false,
  onOpen,
  onMessage,
  onError,
  events = {},
  parseJson = true,
}: SSEOptions<T>) {
  const source = new EventSource(url, {
    withCredentials,
  });

  const parseData = (event: MessageEvent): T => {
    if (!parseJson) {
      return event.data as T;
    }

    try {
      return JSON.parse(event.data) as T;
    } catch {
      return event.data as T;
    }
  };

  source.onopen = (event) => {
    console.log('[SSE] connected');
    onOpen?.(event);
  };

  source.onmessage = (event) => {
    const data = parseData(event);
    onMessage?.(data, event);
  };

  source.onerror = (event) => {
    console.error('[SSE] error', event);
    onError?.(event);
  };

  Object.entries(events).forEach(([eventName, handler]) => {
    source.addEventListener(eventName, (event) => {
      const messageEvent = event as MessageEvent;
      const data = parseData(messageEvent);

      handler(data, messageEvent);
    });
  });

  return {
    source,

    close() {
      source.close();
      console.log('[SSE] closed');
    },

    getReadyState() {
      return source.readyState;
    },
  };
}

使用方式:

const sse = createSSE({
  url: '/api/sse',

  onOpen() {
    console.log('SSE 連線成功');
  },

  onMessage(data) {
    console.log('收到預設 message:', data);
  },

  onError() {
    console.log('SSE 發生錯誤或正在重連');
  },

  events: {
    newMessage(data) {
      console.log('收到 newMessage:', data);
    },

    notification(data) {
      console.log('收到 notification:', data);
    },
  },
});

建立 SSE 連線的核心流程圖:

原本想自己畫流程圖,但發現 AI 畫的不錯,那就直接用吧XD

PS. 若要中斷連線,可使用 close() 內建函式

long polling, short polling, SSE, websocket 之間有什麼差異

polling: 一種資料同步機制。客戶端(如網頁)會設定固定的時間間隔,不斷向伺服器發出請求,詢問是否有新資料。可以分成 Short Polling 和 Long Polling 兩種

bytebytego

bytebytego

從上圖中可以看到不管哪一種 Polling 一開始都是由 Client Side 主動問 Sever Side,而 Short Polling 和 Long Polling 的差異就在發送 request 的頻率

  • Short Polling:每隔一段時間發送 HTTP request
  • Long Polling:請求一次後,Sever Side 有資料才回應;回應完 Client Side 再發起下一次請求

short Polling

setInterval(async () => {
  const res = await fetch('/api/notifications');
  const data = await res.json();
  console.log(data);
}, 3000);

若想像成對話,大概是像以下吧XD

Client: 有新資料嗎?
Server: 沒有喔

3 秒後…

Client: 有新資料嗎?
Server: 沒有喔

3 秒後…

Client: 有新資料嗎?
Server: 有喔,傳給你

優點:簡單明瞭 缺點:浪費 request,而且更新有可能不夠即時

Long Polling

async function poll() {
  const res = await fetch('/api/events');
  const data = await res.json();

  console.log(data);

  poll(); // 收到後立刻發下一次
}

poll();
(腦補對話)

Client: 有新資料嗎?
Server: .....

過了 10 秒…

Server: 有資料了,傳給你
Client: 收到,馬上再問下一次
Client: 有新資料嗎?

優點:比 Short Polling 即時,也比較不浪費 request,但本質上還是一次 request 對一次 response

SSE

開一條 HTTP 長連線(Persistent Connection),Server Side 持續回傳資料

const source = new EventSource('/api/events');

source.onmessage = (event) => { ... };
(腦補對話)

Client: 我要訂閱事件
Server: 好,這條線保持開著

Server: data: 第一筆資料
Server: data: 第二筆資料
Server: data: 第三筆資料

WebSocket

Client Side 和 Server Side 建立一條持久連線(Persistent Connection),雙方都可以主動送資料

const socket = new WebSocket("ws://localhost:3000");

socket.onmessage = (event) => { ... };

socket.onopen = () => {
  socket.send("hello");
};
(腦補對話)

Client A: connect
Server -> Client A: connected

Client B: connect
Server -> Client B: connected

Client A -> Server: Hello, I'm Client A
Server: 收到 Client A 傳來的訊息

Server -> Client B: (傳送訊息給 Client B)
Client B: 收到新訊息:「A: Hello, I'm Client A」

Client B -> Server: Hi~~
Server: 收到 Client B 傳來的訊息

Server -> Client A: (傳送訊息給 Client A)
Client A: 收到新訊息:「B: Hi~~」

各種使用情境整理:

  • 如果只是偶爾更新,例如每 30 秒刷新後台資料 -> Short Polling
  • 如果想做通知,但環境不方便支援 SSE -> Long Polling
  • 如果是 AI 串流、任務進度、log、通知,且環境支援長連線串流 -> SSE
  • 如果是聊天室、遊戲、多人協作、即時雙向互動 -> WebSocket

參考資料

  1. MDN — WebSocket
  2. MDN — SSE
  3. WHATWG SSE

메타데이터
post_id
48b8d43e559b
slug
初探-sse-server-sent-events-48b8d43e559b
url
https://medium.com/@hanforworkandlife/%E5%88%9D%E6%8E%A2-sse-server-sent-events-48b8d43e559b
canonical_url
https://medium.com/@hanforworkandlife/%E5%88%9D%E6%8E%A2-sse-server-sent-events-48b8d43e559b
author_url
https://medium.com/@hanforworkandlife
status
ok
fetched_at
2026-06-24 04:09:36