題幹與適用場景
題目考察前端工程師能否把 WebSocket 當成不可靠連線來管理。瀏覽器可能經歷 Wi-Fi 切換、休眠、代理中斷、伺服器重啟或靜默半開連線;只在 onclose 中立即 connect() 會製造重連風暴,也無法找回斷線期間事件。回答應涵蓋客戶端狀態、退避、心跳、補償同步、鑑權和頁面生命週期。
面試官考察點
強回答會先定義訊息語意和恢復邊界,再用明確狀態機約束連線轉換。它會使用隨機抖動的指數退避、應用層心跳偵測半開連線、序號或游標補拉缺失事件,並讓發送佇列區分可丟棄與必須確認的訊息。還應說明背景分頁、網路變化、token 過期、伺服器限流和使用者可見狀態。
回答前需要釐清的問題
- 訊息是可遺失通知、可重播事件,還是必須一次處理的命令?伺服器是否提供游標補拉?
- 連線鑑權如何刷新?重連時 token 過期、權限變化或協定版本變化怎麼辦?
- 心跳由誰發送和確認?代理可能靜默丟包多久,允許多長偵測時間?
- 需要支援多分頁、行動端背景、瀏覽器休眠和網路離線嗎?
- 重連期間使用者可以繼續編輯嗎?衝突、順序和重複提交如何處理?
30 秒回答框架
「我會把客戶端建模成 idle、connecting、open、suspect、backoff、closed 六態。開啟後使用帶逾時的應用層 ping/pong,異常關閉或心跳逾時進入指數退避並加入隨機抖動,避免所有客戶端同時重連。每個事件帶遞增序號,重連成功先用 last-seen 游標補拉,再開放即時消費;命令採用冪等鍵和確認,通知可以丟棄。頁面隱藏、離線和 token 過期時暫停或重新鑑權,並向使用者顯示連線狀態。」
分步深入解答
第一步:定義連線狀態機
集中定義狀態和合法轉換,例如 connecting -> open -> suspect -> backoff -> connecting;使用者主動關閉進入 closed,不再自動重連。每次轉換記錄原因、嘗試次數和連線 id,方便診斷重複連線。
第二步:區分 close、error 與半開
瀏覽器的 error 不一定提供原因,close 也可能長時間不觸發。應用層心跳記錄發送時間、pong deadline 和最近收到的訊息;逾時後主動關閉舊 socket,再進入退避。
第三步:實作退避與抖動
RFC 6455 警告持續立即重連會形成拒絕服務式重連風暴。可用 min(cap, base * 2^attempt) + random(0, jitter) 計算延遲;成功穩定一段時間後重置次數。伺服器回傳限流或維護提示時,應尊重 Retry-After 或更長冷卻時間。
第四步:恢復事件游標
即時事件帶單調遞增的 stream sequence。客戶端保存最後連續處理的序號,重連握手攜帶該游標;伺服器先返回缺失區間,再切換即時流。若游標過期,伺服器返回快照版本,客戶端載入快照後繼續。
第五步:設計發送佇列與冪等
presence、typing 等通知可丟棄;編輯、支付意圖等命令必須確認。命令帶 clientMessageId,伺服器按冪等鍵去重並返回結果;斷線期間只快取有界佇列,超過上限就阻止繼續提交並提示使用者。
第六步:處理鑑權與協定變化
重連前檢查 token 是否即將過期,必要時先刷新;收到未授權關閉碼時停止盲目重試並進入重新登入。握手帶協定版本,不相容時降級或提示升級,不能在錯誤版本上循環重連。
第七步:結合頁面與網路生命週期
visibilitychange、online/offline 和行動端背景會改變策略。背景頁面可降低心跳頻率或暫停即時流,回到前景後執行健康檢查與游標同步。離線時停止撥號,網路恢復後再按退避啟動。
第八步:監控使用者體驗和伺服器壓力
客戶端記錄連線建立耗時、重連次數、心跳逾時、補拉事件、游標缺口和佇列丟棄數;伺服器觀察並發連線、握手失敗、重連峰值與重複訊息。錯誤日誌不能含 token 或訊息正文。
最小狀態機偽代碼
on_open(socket):
state = OPEN
send({type: "resume", lastSeen: cursor})
on_heartbeat_timeout():
socket.close()
state = BACKOFF
delay = min(MAX, BASE * 2 ** attempts) + random(0, JITTER)
schedule(connect, delay)設計取捨與邊界
| 決策 | 選擇 | 原因 |
|---|---|---|
| 重連延遲 | 截斷指數退避 + 抖動 | 降低同步重連峰值 |
| 斷線恢復 | 游標補拉,必要時快照 | 不必依賴重新整理 |
| 訊息可靠性 | 通知可丟棄,命令冪等確認 | 按業務價值控制成本 |
| 背景頁面 | 降頻或暫停,回前景同步 | 節省電量並減少無效連線 |
WebSocket 只提供有序訊息,不自動提供 exactly-once、離線佇列或狀態同步。可靠性必須由協定層定義;若只需伺服器推送且可接受遺失,也可比較 SSE。
落地計畫與證據
先實作狀態機、心跳和帶抖動退避,再增加游標恢復與冪等命令。用網路切換、伺服器重啟、瀏覽器休眠、token 過期和大量客戶端同時斷線做演練。RFC 6455 建議異常關閉後採用隨機初始延遲與逐步增加退避;MDN WebSocket 文件可作為瀏覽器事件與 readyState 的依據。
試點退出條件
斷線後無需重新整理即可恢復;已確認命令沒有重複或遺失;重連峰值不壓垮伺服器;背景頁面不持續佔用連線;使用者能看到狀態與最後同步時間。
如何證明收益不是巧合
比較改造前後恢復成功率、p95 恢復時間、重複事件率、游標缺口、握手峰值和行動端耗電,並按網路、瀏覽器與頁面可見性分層。
常見誤區與追問
在 onclose 立即重連
伺服器重啟時所有客戶端同時撥號,形成 thundering herd。必須使用退避、抖動和限流提示。
只依賴 close 事件
半開連線可能沒有 close。用心跳和 deadline 判定失聯,再關閉舊 socket。
重連成功就認為資料完整
連線恢復不代表斷線期間事件已到達。用 last-seen 游標補拉與連續序號驗證完整性。
如何避免重複命令?
每個命令使用客戶端冪等鍵,伺服器持久化結果;客戶端收到確認或查到結果後才移除佇列項。
token 在重連時過期怎麼辦?
先刷新並重新握手;未授權關閉碼應停止重試並轉入登入流程,不能當作網路抖動。
多分頁如何控制連線數?
可用 BroadcastChannel 或 SharedWorker 選出一個持有連線的分頁,其餘分頁訂閱事件;owner 失聯時轉移所有權並重新驗證游標。