
HTTP 輪詢是最笨的即時方案。每 2 秒發一個 request 問「有新資料嗎?」「沒有。」「有新資料嗎?」「沒有。」——99% 的 request 是浪費。WebSocket 的本質是建一條長時間維持的雙向通道,有資料的時候才傳,沒資料的時候安安靜靜。
先講結論
WebSocket 不難建,難的是維護:心跳保活、斷線重連、認證、水平擴展。單機跑得很好,多台就會發現 client A 在節點 1、client B 在節點 2,互相收不到訊息。先把這些問題想清楚再上 WebSocket,不然你會發現 polling 其實也沒那麼差。
握手:從 HTTP 升級
WebSocket 連線從一個 HTTP request 開始,帶著 Upgrade: websocket header。Server 回 101 Switching Protocols,之後就是雙向通訊。
重點:認證要在握手階段就做。不要等到 WebSocket 連上了才驗證——那時候連線已經建立,你再踢人就浪費資源。
import { WebSocketServer } from 'ws'
const wss = new WebSocketServer({ noServer: true })
server.on('upgrade', (req, socket, head) => {
const token = new URL(req.url!, 'http://x').searchParams.get('token')
if (!token || !verify(token)) {
socket.destroy()
return
}
wss.handleUpgrade(req, socket, head, ws => {
wss.emit('connection', ws, req)
})
})心跳:連線「假活著」是最常見的坑
WebSocket 連線建好之後,可能因為 NAT timeout、proxy 斷線、手機切網路而「默默斷掉」。Server 端看起來連線還在,但其實已經是一條死路。
心跳就是定期 ping,client 沒回 pong 就判定斷線。
setInterval(() => {
for (const ws of clients) {
if (ws.isAlive === false) return ws.terminate()
ws.isAlive = false
ws.ping()
}
}, 30000)
ws.on('pong', () => {
ws.isAlive = true
})30 秒一次 ping,沒回就踢。不做心跳的話,你的 active connections 數字會越來越大,但大部分都是幽靈連線。
斷線重連:指數退避
Client 斷線後立即重連 1-2 次,之後指數回退:1s → 2s → 4s → 8s,最多 30 秒。不要無限重試——如果 server 掛了,幾千個 client 同時瘋狂重連只會讓事情更糟。
同時要提供 last_event_id,讓 client 重連後能補齊斷線期間遺漏的資料。Server 端保留一個短期事件緩存(比如最近 5 分鐘的事件),重連時從那裡補。
訊息格式:統一就好
{
"type": "chat.message",
"payload": { "roomId": "r1", "text": "hi" },
"trace_id": "a1b2c3"
}type 用 domain.action 格式、payload 放內容、trace_id 方便 debug。格式不用花俏,統一就好。
水平擴展:最難的部分
單機完全沒問題。但上了兩台之後,user A 連到節點 1、user B 連到節點 2,同一個聊天室的訊息互相收不到。
兩種解法:
Sticky Session:Load balancer 讓同一個 user 黏在同一台。簡單但節點掛了就得重連。
Pub/Sub:每個節點都訂閱 Redis Pub/Sub。訊息先發到 Redis,所有節點都收到,再推給各自的 client。這是主流做法。
# Nginx 反代 WebSocket
location /ws/ {
proxy_pass http://ws_backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 60s;
}proxy_read_timeout 要設——Nginx 預設 60 秒沒資料就斷連線。如果你的心跳間隔是 30 秒,這個值至少要 60 秒以上。
安全和節流
必須用 WSS(WebSocket over TLS)。控制每個連線的 message rate——一個 client 一秒發 1000 則訊息就是在攻擊你。檢查 payload size,防止巨型訊息。
什麼時候不用 WebSocket?
只需要 server → client 的單向推送?用 SSE(Server-Sent Events)就好,更簡單、自動重連、不需要處理擴展問題。
長時間沒有互動但偶爾需要更新?用 long polling 或 SSE。WebSocket 每條連線都佔資源,如果大部分時間沒在傳資料,那就是浪費。
WebSocket 就像打電話。雙方可以隨時講話,不用每次都重新撥號。但電話線是有成本的——同時開幾千通電話,你的電信帳單會很精彩。