「你有沒有試過用 setInterval 每秒打一個 API 來模擬即時通知? 能動,但就像用水桶一杓一杓地把海水舀空——你永遠在做無用功。 99% 的時候伺服器根本沒有新資料,你還是每秒發出一次請求。」
HTTP 的設計是「請求-回應」模式:客戶端主動問,伺服器被動答。 但即時應用需要的是「有事才通知」——伺服器主動推送。 這篇帶你理解 WebSocket 和 SSE 這兩個為即時通訊設計的協議, 以及在實際工程中如何選擇和實作它們。
Section 1:HTTP 的問題 — 為什麼需要 WebSocket
在 WebSocket 出現之前,開發者用「輪詢(Polling)」來實作即時功能。 這個方案能動,但代價很高。
用 HTTP Polling 模擬即時更新
HTTP Polling 的問題
- 每次請求都要 TCP 三次握手
- 每次請求都要 HTTP Headers(幾百 bytes)
- 大部分回應是「沒有新資料」
- 延遲等於輪詢間隔(至少幾百 ms)
- 伺服器資源被大量浪費
WebSocket 的優勢
- 只需一次握手建立持久連線
- 訊息 overhead 極小(2-10 bytes header)
- 只在有新資料時傳輸
- 延遲幾乎為零(毫秒級)
- 伺服器可主動推送
HTTP Polling 像是你每秒打電話問「有新消息嗎」,對方說「沒有」,掛掉。
WebSocket 像是打了一個不掛斷的電話,有新消息對方直接說。 你不需要一直問,對方有事就通知你。
補充:還有一種折衷方案叫 Long Polling——伺服器收到請求後不立即回應, 等到有新資料或超時才回應。比 Polling 好一點,但依然有連線 overhead 問題。 現在已被 WebSocket / SSE 取代。
Section 2:WebSocket 協議原理
WebSocket 不是憑空建立的——它建立在 HTTP 之上,先用 HTTP 做「升級握手」, 然後把連線轉換成持久的雙向通道。這個設計讓 WebSocket 可以穿越大多數的防火牆和代理伺服器(因為一開始看起來像普通 HTTP)。
WebSocket 連線建立過程
WebSocket 的關鍵特性
連線特性
- 長連線:建立一次,持續使用,不需要每次握手
- 雙向:客戶端和伺服器都可以主動發訊息
- 低延遲:毫秒級,因為沒有 HTTP 握手 overhead
- 有訊息邊界:不像 TCP stream,每個訊息是獨立的 Frame
訊息框架(Frame)
- Text Frame:UTF-8 文字(JSON 字串)
- Binary Frame:二進位資料(圖片、檔案)
- Ping/Pong Frame:心跳檢測,確認連線存活
- Close Frame:優雅關閉連線,附帶原因碼
WebSocket URL 格式
Section 3:瀏覽器 WebSocket API
瀏覽器原生支援 WebSocket API,不需要任何額外套件。但直接使用原生 API 在生產環境有一些挑戰, 例如斷線重連、錯誤處理等,以下先展示如何正確封裝它。
WebSocket Manager:封裝重連邏輯
React Hook 封裝
在 React 中,WebSocket 需要配合 useRef 和 useEffect來管理連線生命週期,確保元件卸載時正確關閉連線,避免 memory leak。
Section 4:Server-Sent Events(SSE)— 單向推送的輕量選擇
SSE 是建立在 HTTP 之上的單向串流協議:伺服器可以持續推送訊息給客戶端,但客戶端不能反向傳訊息。 聽起來像限制,但很多場景其實只需要這樣——通知、報價、進度更新都是伺服器單向推送的需求。
SSE 的優勢
- 建立在標準 HTTP 上,無需特殊協議升級
- 原生支援斷線自動重連
- 瀏覽器支援良好,包含舊版
- 通過 HTTP/2 可多工(multiplexing)
- 實作簡單,後端一個 Stream 就夠
SSE 的限制
- 只能伺服器 → 客戶端(單向)
- 只能傳文字(不能傳 Binary)
- 每個連線佔用一個 HTTP 連線
- IE / Edge 舊版不支援(現在基本無影響)
SSE 的使用場景
後端實作(Next.js App Router API Route)
SSE 的關鍵在於 Content-Type: text/event-stream 和 特定的資料格式(data: ...\n\n)。 每個事件以兩個換行符結尾。
前端實作(EventSource API)
SSE 的自動重連機制
瀏覽器的 EventSource 原生支援自動重連——斷線後會自動嘗試重新連線。 伺服器可以用 retry: 5000 指定重連間隔(毫秒)。 如果事件有 id: 欄位,重連時會帶 Last-Event-ID header, 讓伺服器知道從哪條事件繼續推送,避免資料遺失。
Section 5:Socket.io — 生產環境的首選
直接使用原生 WebSocket API 在生產環境有以下挑戰:需要自己實作重連邏輯、自己管理 heartbeat、 不同環境(有些代理不支援 WebSocket)行為不一致、沒有 room / namespace 概念、 規模化需要自己處理跨伺服器的訊息廣播。
Socket.io 是一個封裝在 WebSocket 上的函式庫,解決了所有這些問題, 並在 WebSocket 不可用時自動退回 HTTP Long Polling。
後端(Node.js + Socket.io)
前端(React + Socket.io-client)
Socket.io 解決了哪些原生 WebSocket 的痛點
✅ 自動重連,有指數退避邏輯
✅ 內建 Heartbeat(Ping/Pong)
✅ Namespace:同一連線多個頻道
✅ Room:分組廣播
✅ 降級支援:WebSocket → Long Polling
✅ Acknowledgement:確認訊息已收到
✅ Redis Adapter:多節點水平擴展
✅ 完整的 TypeScript 支援
Section 6:選擇指南
現在你了解了三種方案的特性,以下這份表格幫你在具體需求下快速做出選擇:
| 需求場景 | 建議方案 | 原因 |
|---|---|---|
| 聊天室、即時訊息 | Socket.io | 雙向通訊,需要 Room 管理,生產環境穩定 |
| 即時通知(系統推送) | SSE | 單向推送,實作簡單,HTTP 友好 |
| 股票報價、即時數據流 | SSE | 伺服器單向推送,低延遲,簡單易維護 |
| 多人協作(Google Docs 類) | WebSocket | 需要雙向低延遲,操作變換同步 |
| 進度條、任務狀態更新 | SSE | 伺服器推進度,客戶端不需要傳資料 |
| 遊戲、低延遲互動 | WebSocket | 毫秒級延遲,雙向高頻訊息 |
| AI 串流輸出(打字效果) | SSE | 伺服器逐步推 token,ChatGPT 就是這樣做的 |
| 需要水平擴展的生產環境 | Socket.io + Redis | Redis Adapter 讓多個 Node.js 實例可以廣播 |
生產環境注意事項
連線管理
- 連線數限制:每個 WebSocket 連線佔用伺服器 file descriptor 和記憶體。 一台普通伺服器能穩定支援約 1 萬個並發連線,超過需要水平擴展。
- 記憶體估算:每個 Socket.io 連線約佔 20-30 KB 記憶體。 10 萬個連線 ≈ 2-3 GB RAM。
架構考量
- Load Balancer:WebSocket 需要設定 sticky session(同一用戶的請求路由到同一節點), 或使用 Redis Pub/Sub 讓所有節點共享訊息。
- HTTPS 環境:生產環境必須用
wss://(WebSocket Secure)。 純 HTTP 的ws://在 HTTPS 頁面會被瀏覽器阻擋(Mixed Content)。