用 WebSocket 做即時通訊,demo 很容易跑通:瀏覽器連線伺服器端,雙向收發訊息。但上線後問題集中在三處:token 鑑權不嚴謹導致連線被濫用;斷線後離線訊息遺失;多端在線時訊息順序不一致。本文按連線建立、訊息幀設計、伺服器端推送、斷線恢復的順序提供可執行程式碼,並在每個環節標注與現成 IM 軟體的差距,幫你在動手前判斷自建是否划算。
建立 WebSocket 連線:用戶端與伺服器端的握手與鑑權
用戶端程式碼:
const socket = new WebSocket(
`wss://example.com/ws?token=${encodeURIComponent(token)}`
);
socket.onopen = () => console.log('connected');
socket.onerror = (e) => console.error('ws error', e);
伺服器端用 Node.js 的 ws 程式庫處理升級:
const { WebSocketServer } = require('ws');
const wss = new WebSocketServer({ noServer: true });
server.on('upgrade', (req, socket, head) => {
const url = new URL(req.url, 'http://localhost');
const token = url.searchParams.get('token');
if (!verifyToken(token)) {
socket.write('HTTP/1.1 401 Unauthorized\r\n\r\n');
socket.destroy();
return;
}
wss.handleUpgrade(req, socket, head, (ws) => {
wss.emit('connection', ws, req);
});
});
不要把 token 塞進 Sec-WebSocket-Protocol,部分代理和用戶端會丟棄或截斷。正式環境可在查詢參數中只放一次性 ticket,伺服器端校驗後綁定使用者身分並下發短時 session id;連線建立後用戶端再透過首個訊息攜帶簽名完成二次校驗。現成 IM SDK 通常已封裝握手、鑑權和重連,開發者不需要處理這些邊界。
定義統一訊息訊框:上行與下行結構
所有訊息使用同一 JSON 結構,seq" 由用戶端生成並單調遞增,用於確認與排序。"
{
"type": "chat",
"seq": 1024,
"to": "user_42",
"payload": { "text": "hello" }
}
{
"type": "chat",
"seq": 1024,
"from": "user_7",
"to": "user_42",
"ts": 1710000000000,
"payload": { "text": "hello" }
}
"伺服器端返回 ack 時攜帶相同 "seq";用戶端在收到 ack 前保留副本用於重試。群聊把 "to" 換成 "room_id",伺服器端負責展開接收人。不要依賴 WebSocket 的單一連線順序:同一使用者多端上線時,必須由伺服器端按 "seq" 排序後推送,否則多端可能顯示順序不一致。"
伺服器端推送:從單一連線到房間/使用者定向
const clients = new Map(); // userId -> Set<WebSocket>
function addClient(userId, ws) {
if (!clients.has(userId)) clients.set(userId, new Set());
clients.get(userId).add(ws);
ws.on('close', () => {
const set = clients.get(userId);
if (!set) return;
set.delete(ws);
if (set.size === 0) clients.delete(userId);
});
}
function sendToUser(userId, message) {
const sockets = clients.get(userId);
if (!sockets) return false;
for (const ws of sockets) {
if (ws.readyState === 1) {
ws.send(JSON.stringify(message));
}
}
return true;
}
"連線註冊表必須處理多端上線和異常斷線,否則 "Set" 會無限增長。單聊同時寫入傳送方與接收方;群聊先查詢房間成員再廣播。傳送時若連線非 OPEN,應把訊息寫入離線佇列,不能直接丟棄。伺服器端推送延遲升高時,按網路、伺服器端、用戶端分層的定位方法見"即時通訊延遲高怎麼排解?從網路、伺服器端到用戶端的分層定位步驟。
斷線重連與心跳:訊息不丟的邊界
- 用戶端每 30 秒傳送
{"type":"heartbeat"};伺服器端 35 秒內未收到任何幀,判定連線失效並關閉。 - 用戶端
onclose後按 1s、2s、4s、8s、封頂 30s 退避重連;重連成功後先拉取本機最後seq之後的訊息。 - 伺服器端不為長時間離線使用者補發訊息,除非你在伺服器端持久化訊息並維護游標;若要求離線必達,必須落盤、補拉、冪等去重。
WebSocket 只解決雙向通道,不提供訊息必達、離線儲存或多端同步。這些能力的具體實作可參考保障訊息必達的關鍵機制:即時通訊訊息不丟失怎麼實作與離線訊息總丟失?即時通訊離線訊息收不到的常見原因與修復。
自建 WebSocket 與現成 IM 軟體功能對比評測
| 維度 | 自建 WebSocket | 現成 IM 軟體 |
|---|---|---|
| 訊息可靠性 | 需自己實作 ack、重試、冪等 | 內建確認、重試與補發策略 |
| 離線訊息 | 需持久化並維護游標 | 預設提供離線佇列與補拉 |
| 多端同步 | 需服務端按 seq 排序推送 | 已處理多端游標與已讀狀態 |
| 後台管理 | 需另建控制台 | 提供成員、群組、審計等後台 |
| 維運成本 | 長期監控、擴容、修復 | 供應商維護,按訂閱付費 |
如果你的團隊只有前端,或需要在兩週內上線多端同步與離線訊息,自建通常不划算;可先按預算有限的中小企業即時通訊選型,重點核對這5個維度驗證需求。已經決定自建時,先算清長期成本,參考成本拆解:自建即時通訊需要多少錢,包含哪些隱性支出。
常見問題
WebSocket 能保證訊息必達嗎?
不能。WebSocket 只提供全雙工通道,斷線、伺服器端故障、用戶端未 ack 都可能丟訊息。必達需要持久化、seq 補拉、ack 重試和冪等去重。
心跳間隔多少合適?
桌面端通常 30 秒,伺服器端 35 秒無任何幀判定斷線;行動端可放寬到 60 秒,但必須監聽網路切換並主動重連。
沒有後端經驗能快速實作即時通訊嗎?
能跑通單頁面 demo,但處理離線訊息、多端同步和維運需要後端與儲存基礎。沒有後端建議直接使用現成 SDK 或商用 IM。
什麼時候該停止自建,直接採購?
當需求包含離線必達、多端同步、後台管理、稽核合規中的任意兩項以上,或上線時間少於兩週時,採購商用 IM 通常比自建更穩、更便宜。
