目录
- [一、WebSocket 概念](#一、WebSocket 概念)
- [二、WebSocket 生命周期](#二、WebSocket 生命周期)
- [三、websocket 协议请求过程(⭐️⭐️⭐️⭐️⭐️)](#三、websocket 协议请求过程(⭐️⭐️⭐️⭐️⭐️))
-
- 0、整体流程总览(先有一张"图")
- [1、第 1 步:前端发起 WebSocket 请求](#1、第 1 步:前端发起 WebSocket 请求)
- [2、第 2 步:HTTP → WebSocket Upgrade 请求(重点)](#2、第 2 步:HTTP → WebSocket Upgrade 请求(重点))
- [3、第 3 步:服务端返回 101(协议切换)](#3、第 3 步:服务端返回 101(协议切换))
- [4、第 4 步:WebSocket 数据帧通信(真正开始)](#4、第 4 步:WebSocket 数据帧通信(真正开始))
- [5、第 5 步:心跳(Ping / Pong)](#5、第 5 步:心跳(Ping / Pong))
- [6、第 6 步:关闭连接(Close 帧)](#6、第 6 步:关闭连接(Close 帧))
- 7、抓包视角(非常加分)
- [四、前端 WebSocket 工程级实现(重点)](#四、前端 WebSocket 工程级实现(重点))
-
- 1、正确的职责划分(非常重要)
- [2、前端使用 WebSocket 的最小正确姿势](#2、前端使用 WebSocket 的最小正确姿势)
- [3、前端 WebSocket 必备技能(核心)(⭐️⭐️⭐️⭐️⭐️)](#3、前端 WebSocket 必备技能(核心)(⭐️⭐️⭐️⭐️⭐️))
-
- (1)、连接生命周期管理
- [(2)、在线 / 离线状态感知](#(2)、在线 / 离线状态感知)
-
- ①、网络状态感知
- ②、页面可见性感知
- ③、与连接生命周期结合
- [④、微小示例(结合 WebSocket SDK)](#④、微小示例(结合 WebSocket SDK))
- ⑤、总结表格
- (3)、连接复用与资源治理
-
- [①、单例 / 连接池](#①、单例 / 连接池)
- [②、动态订阅 / 退订](#②、动态订阅 / 退订)
- ③、统一资源治理
- ④、多模块共享消息分发
- ⑤、微小示例(结合前面的总线示例)
- ⑥、总结表格
- (4)、消息协议与版本演进
-
- ①、标准化字段
- [②、ACK / 去重 / 顺序机制](#②、ACK / 去重 / 顺序机制)
- ③、协议版本管理
- [④、模块化 / 类型化](#④、模块化 / 类型化)
- ⑤、微小示例
- ⑥、协议演进策略总结
- (5)、消息可靠性保障
- (6)、消息分发与消费模型
-
- ①、创建一个简单消息总线(Dispatcher)
- [②、WebSocket 封装](#②、WebSocket 封装)
- ③、多模块消费示例
- (7)、架构解耦与职责分离
-
- [①、封装 WebSocket 客户端](#①、封装 WebSocket 客户端)
- [②、消息总线 / Dispatcher](#②、消息总线 / Dispatcher)
- [③、SDK / Service 层(业务层接口)](#③、SDK / Service 层(业务层接口))
- [④、UI / 业务层(消费层)](#④、UI / 业务层(消费层))
- ⑤、额外加分点
- [4、一个可复用的 WebSocket Client(简化示例)](#4、一个可复用的 WebSocket Client(简化示例))
- [5、前端 WebSocket 常见翻车点(血泪)](#5、前端 WebSocket 常见翻车点(血泪))
- [五、服务端 WebSocket 架构要点](#五、服务端 WebSocket 架构要点)
-
- [1、单机 WebSocket ≠ 可用系统](#1、单机 WebSocket ≠ 可用系统)
- [2、标准 WebSocket 集群架构](#2、标准 WebSocket 集群架构)
- [3、Session 粘性(Sticky Session)](#3、Session 粘性(Sticky Session))
- [六、认清 WebSocket 和 SSE](#六、认清 WebSocket 和 SSE)
-
- [1、WebSocket 和 SSE对比](#1、WebSocket 和 SSE对比)
- [2、最容易选错的 6 个场景(血泪)](#2、最容易选错的 6 个场景(血泪))
- [七、认清「HTTP、WebSocket」和 「Socket」](#七、认清「HTTP、WebSocket」和 「Socket」)
-
- 1、先给你一张「总关系图」(先立世界观)
- [2、HTTP 与 WebSocket 对比](#2、HTTP 与 WebSocket 对比)
- [八、WebSocket 在典型系统中的位置](#八、WebSocket 在典型系统中的位置)
-
- [1、IM 系统(时通讯系统)------人与人、人与群实时交流的系统](#1、IM 系统(时通讯系统)——人与人、人与群实时交流的系统)
- [2、IoT 系统(物联网系统)------"管理、监控、控制大量设备"的系统](#2、IoT 系统(物联网系统)——“管理、监控、控制大量设备”的系统)
- [3、AI 实时系统------AI 推理结果"边算边给用户"的系统](#3、AI 实时系统——AI 推理结果“边算边给用户”的系统)
- [九、WebSocket 的致命坑(真实血泪)](#九、WebSocket 的致命坑(真实血泪))
一、WebSocket 概念
WebSocket 是"实时交互通道",不是"状态通知工具"。
WebSocket 的本质(先纠正 90% 的误解):
- WebSocket = 基于 HTTP 握手 + 基于 TCP 的全双工应用层协议
为什么 WebSocket 会被发明?
HTTP 的根本问题只有一个:
- 服务端不能主动说话
所有"实时方案"的演进路径:
typescript
短轮询
→ 长轮询
→ HTTP Streaming
→ SSE
→ WebSocket
WebSocket 是 "彻底摆脱请求-响应模型" 的方案。
二、WebSocket 生命周期
typescript
CONNECTING
→ OPEN
→ CLOSING
→ CLOSED
前端状态映射:
typescript
WebSocket.CONNECTING = 0
WebSocket.OPEN = 1
WebSocket.CLOSING = 2
WebSocket.CLOSED = 3
三、websocket 协议请求过程(⭐️⭐️⭐️⭐️⭐️)
0、整体流程总览(先有一张"图")

一句话描述 websocket 请求过程:
- WebSocket = HTTP Upgrade 握手 + 基于 TCP 的长连接 + 自定义的全双工二进制帧协议
typescript
前端 JS
↓
HTTP Upgrade 请求
↓
服务器 101 Switching Protocols
↓
WebSocket 连接建立
↓
数据帧双向通信
↓
Close 帧关闭连接
‼️ 关键认知:
- WebSocket 的"第一次请求"本质是 HTTP
1、第 1 步:前端发起 WebSocket 请求
typescript
const ws = new WebSocket('wss://example.com/ws')
浏览器内部发生的事:
- 并不是直接走 WebSocket
- 而是先发一个 HTTP 请求
- 请求头中带 Upgrade: websocket
2、第 2 步:HTTP → WebSocket Upgrade 请求(重点)
客户端请求(HTTP):
bash
GET /ws HTTP/1.1
Host: example.com
Connection: Upgrade
Upgrade: websocket
Sec-WebSocket-Version: 13
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: https://example.com
🔥 各字段含义(面试必问):
| Header | 作用 |
|---|---|
Upgrade: websocket |
请求升级协议 |
Connection: Upgrade |
告诉中间层不能降级 |
Sec-WebSocket-Key |
随机字符串,用于校验 |
Sec-WebSocket-Version |
协议版本(固定 13) |
Origin |
安全校验(CORS 类似) |
3、第 3 步:服务端返回 101(协议切换)
服务端响应:
bash
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
核心校验逻辑(重要):
typescript
Sec-WebSocket-Accept =
base64(
SHA1(
Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
)
)
目的:
- 防止普通 HTTP 客户端伪造 WebSocket 请求
到这里为止,发生了什么本质变化?
| 对比 | HTTP | WebSocket |
|---|---|---|
| 连接 | 一次一请求 | 长连接 |
| 协议 | 文本 | 二进制帧 |
| 状态 | 无状态 | 有状态 |
| 通道 | 请求-响应 | 全双工 |
📌 HTTP 协议生命周期在 101 之后结束
4、第 4 步:WebSocket 数据帧通信(真正开始)
WebSocket 帧结构(理解级):
typescript
| FIN | OPCODE | MASK | Payload length | Payload data |
常见 OPCODE:
| Opcode | 含义 |
|---|---|
0x1 |
文本帧 |
0x2 |
二进制帧 |
0x8 |
关闭 |
0x9 |
Ping |
0xA |
Pong |
前端发送的本质:
ws.send(JSON.stringify({ type: 'CHAT', msg: 'hi' }))
会被浏览器封装成 WebSocket Frame
5、第 5 步:心跳(Ping / Pong)
为什么一定要心跳?
- NAT / 防火墙会回收空闲 TCP
- 服务端无法感知客户端是否"假死"
典型策略:
| 方向 | 类型 |
|---|---|
| Client → Server | Ping |
| Server → Client | Pong |
⚠️ 浏览器 API 不允许手发 Ping 帧
- 只能业务层发"心跳消息"
6、第 6 步:关闭连接(Close 帧)
前端主动关闭:
typescript
ws.close(1000, 'normal close')
协议层流程:
typescript
Client → Close Frame
Server → Close Frame
TCP 连接关闭
常见 Close Code:
| Code | 含义 |
|---|---|
1000 |
正常 |
1006 |
异常断开 |
1008 |
策略违规 |
7、抓包视角(非常加分)
typescript
Chrome DevTools → Network → WS
你会看到:
- 第一条:HTTP 101
- 后续:Message / Frame
- Ping / Pong
- Close
🔥 这是 WebSocket 面试"王炸现场证明"
四、前端 WebSocket 工程级实现(重点)
1、正确的职责划分(非常重要)
❌ 错误:
typescript
React/Vue Component
└── new WebSocket()
✅ 正确:
typescript
WebSocketService
↓
State Store(Pinia / Zustand)
↓
UI
2、前端使用 WebSocket 的最小正确姿势
typescript
const ws = new WebSocket('wss://api.example.com/ws')
ws.onopen = () => console.log('connected')
ws.onmessage = (e) => {
const msg = JSON.parse(e.data)
console.log(msg)
}
ws.onerror = console.error
ws.onclose = () => console.log('closed')
⚠️ 这段代码只能算"能跑",不能上生产
3、前端 WebSocket 必备技能(核心)(⭐️⭐️⭐️⭐️⭐️)
前端 WebSocket 必备的 7 大核心能力:
- 连接生命周期管理(自动重连 + 心跳机制)
- 在线 / 离线状态感知
- 消息协议与版本演进
- 消息可靠性保障(消息 ACK / 去重)
- 连接复用与资源治理
- 架构解耦与职责分离(与 UI 解耦)
- 消息分发与消费模型
这 7 大能力不是并列的,而是"自底向上、逐层依赖"的能力栈:
typescript
重连/心跳
↓
在线感知
↓
复用连接
↓
协议结构
↓
ACK / 去重
↓
消息路由
↓
UI 解耦
(1)、连接生命周期管理
连接生命周期管理是前端 WebSocket 的基石,核心就是"断线自动重连 + 心跳检测",确保连接长期可用、稳定可靠,同时可结合页面可见性和在线状态优化资源。
定义:
- 连接生命周期管理 = 保证 WebSocket 连接能长期存活、可恢复、稳定可靠,无论网络波动、浏览器状态如何。
核心目标:
- 连接 断线自动恢复
- 连接 不死锁 / 假死
- 尽量节省资源,避免频繁重连
- 在不同浏览器状态下(前台 / 后台 / 离线)仍然安全
WebSocket 自身只是 TCP + 升级协议,不提供:
- 自动重连
- 连接状态感知
- 心跳检测
现实网络环境中:
| 场景 | 问题 | 后果 |
|---|---|---|
| 网络抖动 / 切换 Wi-Fi | TCP 断开,浏览器不报错 | 消息丢失,UI 假死 |
| 页面切后台 | 某些浏览器暂停 JS | 连接可能超时 |
| 心跳缺失 | NAT / 防火墙断开 | 长连接被意外切断 |
因此自动重连 + 心跳机制是前端 WebSocket 的必做能力。
核心模块:
- 自动重连(指数退避)
- 心跳机制
①、自动重连(指数退避)
目标:
- 避免网络短断导致消息丢失,减少服务器压力
策略:
- 断线后立即尝试重连 → 如果失败 → 延迟重试
- 延迟逐步增加 → 指数退避
- 最大重试次数 / 最大延迟限制
伪代码示例:
typescript
let attempt = 0
function reconnect() {
const delay = Math.min(1000 * 2 ** attempt, 30000) // 最大30s
setTimeout(() => {
ws = new WebSocket(url)
ws.onopen = () => { attempt = 0 } // 成功后重置
ws.onclose = () => { attempt++; reconnect() }
}, delay)
}
②、心跳机制(必做)
目标:
- 检测连接是否仍然活着
策略:
- 客户端定时发送心跳(或 Ping 消息)
- 服务端返回 Pong / ACK
- 超时未响应 → 判定连接失效 → 自动重连
示例:
typescript
function startHeartbeat(ws: WebSocket) {
const interval = 10000 // 每10秒
const timer = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) {
ws.send(JSON.stringify({ type: "PING" }))
} else {
clearInterval(timer)
}
}, interval)
}
③、与状态感知结合(可选高级)
- 页面可见性 → 暂停或加长心跳间隔
- navigator.onLine → 离线暂停重连,在线恢复
④、连接生命周期管理总结表
| 能力 | 核心目标 | 实现方式 |
|---|---|---|
| 自动重连 | 网络断开自动恢复 | 指数退避 + 最大重试次数 |
| 心跳机制 | 检测假死 / NAT 超时 | 定时 PING + 超时重连 |
| 状态感知 | 节省资源 / 避免错误重连 | 页面可见性 / 在线状态 |
(2)、在线 / 离线状态感知
"在线 / 离线状态感知是前端 WebSocket 的能力之一,它通过网络状态和页面可见性来判断是否维持连接或暂停心跳,从而保证长连接稳定、节省资源,并结合重连机制处理网络恢复。"
在线 / 离线状态感知:
- 前端能够感知用户网络状态和页面可见性,从而决定 WebSocket 是否需要维持、重连或暂停连接。
核心目标:
- 避免在用户离线或后台时浪费连接资源
- 提高 WebSocket 稳定性(减少假死 / 意外断开)
- 优化用户体验(断网/恢复网时消息可靠性)
WebSocket 本身只保证 TCP 层的传输可靠,但不关注:
- 用户是否离线
- 浏览器是否切后台
- 网络切换(Wi-Fi ↔ 蜂窝网络)
现实问题:
| 场景 | 问题 | 后果 |
|---|---|---|
| 用户断网 | TCP 假死,消息无法发送 | 消息丢失或重连失败 |
| 页面切后台 | 部分浏览器暂停 JS | 心跳/重连不触发 → 长连接被断开 |
| 网络切换 | IP 变化导致 TCP 断开 | 需要重新建立连接 |
所以必须在 SDK 层感知在线/离线状态,并结合连接生命周期管理做决策。
核心模块:
- 离线状态感知
- 页面可见性处理
①、网络状态感知
- 使用浏览器 API:
typescript
window.addEventListener("online", () => {
console.log("网络已连接")
ws.reconnect()
})
window.addEventListener("offline", () => {
console.log("网络已断开")
ws.pause() // 暂停发送或心跳
})
- navigator.onLine 可获取初始状态
- 可以结合 心跳 + ACK 做二次判断
②、页面可见性感知
- 使用 Page Visibility API:
typescript
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") {
ws.resume() // 恢复心跳 / 重连
} else {
ws.pauseHeartbeat() // 暂停心跳,节省资源
}
})
- 后台页可适当延长心跳间隔,降低 CPU / 网络消耗
③、与连接生命周期结合
- 在线 + 页面可见 → 保持连接 + 正常心跳
- 离线 / 页面隐藏 → 暂停或降低心跳频率
- 网络恢复 → 自动重连 + 发送未 ACK 消息
④、微小示例(结合 WebSocket SDK)
typescript
class WSClient {
ws: WebSocket
online: boolean = navigator.onLine
constructor(url: string) {
this.initWS(url)
this.setupListeners()
}
initWS(url: string) {
this.ws = new WebSocket(url)
this.ws.onopen = () => console.log("连接已建立")
this.ws.onclose = () => {
if (this.online) this.reconnect()
}
}
setupListeners() {
window.addEventListener("online", () => {
this.online = true
console.log("网络恢复,尝试重连")
this.reconnect()
})
window.addEventListener("offline", () => {
this.online = false
console.log("网络断开,暂停心跳")
this.pauseHeartbeat()
})
document.addEventListener("visibilitychange", () => {
if (document.visibilityState === "visible") this.resumeHeartbeat()
else this.pauseHeartbeat()
})
}
reconnect() { /* 指数退避重连逻辑 */ }
pauseHeartbeat() { /* 停止心跳 */ }
resumeHeartbeat() { /* 恢复心跳 */ }
}
特点:
- 网络断开 → 自动暂停
- 网络恢复 → 自动重连
- 页面切后台 → 调整心跳
- 页面切前台 → 恢复心跳
⑤、总结表格
| 能力 | 核心目标 | 实现方式 |
|---|---|---|
| 网络状态感知 | 判断在线 / 离线 | window.online/offline |
| 页面可见性感知 | 节省资源 / 避免后台假死 | document.visibilitychange |
| 与连接生命周期结合 | 自动重连 / 心跳调整 | SDK 中的重连 + 心跳逻辑 |
(3)、连接复用与资源治理
连接复用与资源治理是前端 WebSocket 的能力之一,通过单例或连接池统一管理 WebSocket,结合消息总线让多个模块共享连接,同时统一处理心跳、重连和消息分发,从而节省资源并提高系统稳定性。
连接复用与资源治理:
- 前端通过 复用同一 WebSocket 连接服务多个模块/业务,并合理管理连接资源(心跳、重连、断开),避免资源浪费和重复连接。
核心目标:
- 避免每个模块单独创建 WebSocket → 节省 TCP 连接和浏览器资源
- 提高消息分发效率 → 单一连接即可服务多模块
- 管理连接生命周期 → 心跳、重连统一管理
- 支持模块动态订阅/退订 → 高度灵活
如果不复用连接,常见问题:
| 问题 | 后果 |
|---|---|
| 每个模块都创建独立连接 | 消耗 TCP 资源,心跳 / 重连重复计算 |
| 多模块共享消息没有路由 | 消息处理混乱,UI 难维护 |
| 未统一管理 | 重连策略不同,容易出现假死或资源泄露 |
企业级系统中,尤其 IM / 实时推送系统,单页面多模块共享单个 WebSocket 是标配。
①、单例 / 连接池
- 单例连接:整个页面只有一个 WebSocket 实例,所有模块共享
- 连接池(高级):业务量大时,可以维护少量长连接,按模块 / topic 分配
typescript
// 单例模式示例
class WSConnectionManager {
private static instance: WSConnection
static getInstance(url: string) {
if (!WSConnectionManager.instance) {
WSConnectionManager.instance = new WSConnection(url)
}
return WSConnectionManager.instance
}
}
②、动态订阅 / 退订
- 模块只订阅自己关心的消息
- 不需要关闭整个连接 → 节省资源
typescript
const ws = WSConnectionManager.getInstance("wss://example.com/ws")
const unsubChat = ws.bus.subscribe("CHAT", msg => console.log("聊天消息:", msg.payload))
const unsubOrder = ws.bus.subscribe("ORDER", msg => console.log("订单消息:", msg.payload))
// 模块卸载
unsubChat()
③、统一资源治理
- 心跳和重连统一管理
- 模块无需关心连接状态
- 避免重复心跳、重复重连
typescript
class WSConnection {
private heartbeatTimer: any
startHeartbeat() { /* 统一心跳逻辑 */ }
stopHeartbeat() { /* 暂停 */ }
}
④、多模块共享消息分发
- 消息总线分发 → 每个模块独立消费
- 支持消息 ACK / 去重 / 顺序控制
- ws.bus.publish(msg) // 单条消息推送到所有订阅模块
⑤、微小示例(结合前面的总线示例)
typescript
// WSClient 是单例连接
const ws = WSClient.getInstance("wss://example.com/ws")
// 模块 A
ws.subscribe("CHAT", msg => console.log("模块A聊天消息:", msg.payload))
// 模块 B
ws.subscribe("ORDER_STATUS", msg => console.log("模块B订单状态:", msg.payload))
// 动态退订
const unsub = ws.subscribe("NOTIFICATION", msg => console.log(msg))
unsub() // 模块卸载时退订
特点:
- 一个连接服务多个模块
- 动态订阅 / 退订
- 心跳、重连、消息分发统一管理
- 资源高效利用
⑥、总结表格
| 能力 | 核心目标 | 实现方式 |
|---|---|---|
| 单例 / 连接池 | 避免重复连接 | 单例模式或少量连接池 |
| 消息总线 | 多模块共享消息 | Dispatcher / PubSub |
| 动态订阅 / 退订 | 模块自由挂载卸载 | subscribe/unsubscribe |
| 统一心跳/重连 | 节省资源 / 提升稳定性 | SDK 统一管理 |
| 消息可靠性 | 不丢失 / 去重 / 顺序 | ACK + msgId + seq |
(4)、消息协议与版本演进
前端 WebSocket 的消息协议设计不仅要标准化字段、支持 ACK / 去重 / 顺序,还必须设计协议版本号以支持平滑升级。通过向前、向后兼容策略,可以保证新老客户端共存,确保消息可靠性和系统可扩展性。
消息协议与版本演进:
- WebSocket 消息协议 = 前端与服务端约定的 消息格式、字段、类型、ACK、心跳、序列号等规则
- 版本演进 = 当协议更新时,能兼容旧客户端、支持新功能,同时保证可靠性和可扩展性
核心目标:
- 保证前后端通信一致 → 避免解析错误
- 支持新功能 → 不破坏已有模块
- 支持消息可靠性机制 → ACK、去重、顺序
- 可扩展性强 → 新模块可以接入,不影响老模块
为什么重要?
- 不随便发 JSON:随意 JSON 无法保证版本兼容、ACK、去重、顺序等
- 业务复杂时:一个连接可能有多模块多业务,消息字段多,必须标准化
- 升级迭代:如果协议没有版本管理,客户端升级后可能无法解析旧消息
- 可靠性保障:消息 ACK / 去重 / 顺序需要协议支持
所以企业级 WebSocket 消息协议设计是 SDK 核心能力之一。
核心设计原则:
①、标准化字段
每条消息统一格式,例如:
typescript
interface WSMessage {
version: number // 协议版本
type: string // 消息类型
msgId: string // 消息唯一 ID(去重)
seq?: number // 顺序号
payload: any // 业务数据
timestamp?: number // 时间戳
}
- version → 协议版本,兼容不同客户端
- msgId + seq → ACK / 去重 / 顺序
- type → 消息路由
- payload → 业务层数据
②、ACK / 去重 / 顺序机制
- 消息协议必须支持 应用层 ACK
- msgId 用于去重
- seq 用于顺序敏感业务
typescript
// ACK 示例
{
type: "ACK",
ackMsgId: "uuid-001",
status: "RECEIVED",
version: 2
}
③、协议版本管理
- 前端解析消息时根据 version 判断如何处理
- 服务端可以同时维护多版本消息
- 升级策略:
| 类型 | 做法 |
|---|---|
| 向前兼容 | 新字段添加在 payload 内,可选,不影响旧客户端 |
| 向后兼容 | 保留旧字段,老客户端可忽略新字段 |
| 强制升级 | 协议版本差异过大 → 提示客户端更新 |
④、模块化 / 类型化
- 每个消息类型独立定义结构
- 支持多模块消费同一连接消息
- 避免混乱的"万能 JSON"
typescript
// 聊天消息
interface ChatMessage {
text: string
fromUserId: string
toUserId?: string
}
// 订单消息
interface OrderStatusMessage {
orderId: string
status: "PAID" | "CANCELLED"
}
⑤、微小示例
typescript
const wsMessage = {
version: 2,
type: "CHAT",
msgId: "uuid-123",
seq: 1001,
timestamp: Date.now(),
payload: {
text: "hello",
fromUserId: "u001",
}
}
// 客户端解析
function handleMessage(msg) {
if (msg.version === 1) {
// 旧协议解析
} else if (msg.version === 2) {
// 新协议解析
}
}
⑥、协议演进策略总结
| 方向 | 原则 | 举例 |
|---|---|---|
| 新字段添加 | 可选字段,不破坏旧客户端 | payload 新增 avatarUrl |
| 字段修改 | 保留旧字段,标记废弃 | status 新增值,不删除旧值 |
| 消息类型增加 | 新模块订阅新类型 | NOTIFICATION |
| 强制升级 | 协议版本号大幅变化 | 老客户端提示更新 |
(5)、消息可靠性保障
消息可靠性保障 = 消息 ACK + 去重 + 顺序控制 + 重连重发,保证前端业务逻辑在网络抖动下也能正确运行。
定义:
- 消息可靠性保障 = 确保消息 不丢、不重复、按需确认,在前端 WebSocket 上,核心是 消息 ACK、去重和顺序控制。
目的:
- 避免消息丢失 → 用户体验或业务状态错误
- 避免重复处理 → 数据幂等问题
- 确保顺序 → 订单状态、聊天消息等顺序敏感
WebSocket 本身只保证 TCP 层传输可靠:
- TCP 保证 数据到达,但:
- 浏览器刷新 / 网络切换 → WebSocket 断开
- 消息到达客户端,但应用层未处理 → 可能丢失
- TCP 不提供 应用级 ACK → 无法保证业务正确性
- TCP 不提供 去重 / 顺序业务层处理 → 必须在应用层处理
所以可靠性必须在 前端 SDK / 消息协议层实现。
前端可靠性策略汇总表:
| 机制 | 目的 | 实现方式 |
|---|---|---|
| 消息 ACK | 确认已收到 | 客户端返回 ACK / 服务端重发未 ACK |
| 消息去重 | 避免重复处理 | msgId / seq + Set / Map |
| 顺序控制 | 顺序敏感 | seq / timestamp + 缓存重排 |
| 心跳检测 | 避免假死 | Ping / Pong 或业务心跳 |
| 重连 & 重发 | 网络断开 | 指数退避 + 未 ACK 消息重发 |
核心模块:
- 消息 ACK(避免消息丢失)→ 用户体验或业务状态错误
- 消息去重→ 数据幂等问题
- 顺序控制→ 订单状态、聊天消息等顺序敏感
①、消息 ACK(确认机制)
- 服务端发送消息后,客户端 返回确认
- 客户端处理成功才算 ACK
- 服务端可重发未 ACK 消息
协议设计示例:
typescript
// 消息
{
type: "CHAT",
msgId: "uuid-001",
payload: { text: "hello" }
}
// ACK
{
type: "ACK",
ackMsgId: "uuid-001",
status: "RECEIVED"
}
②、消息去重
- 避免同一条消息被多次处理
- 通常用 msgId 或 seq 去重
typescript
const receivedIds = new Set()
function handleMessage(msg) {
if (receivedIds.has(msg.msgId)) return
receivedIds.add(msg.msgId)
// 正常处理
}
③、顺序保证(可选 / 业务层)
- 对顺序敏感的业务,需要 seq 或 timestamp
- 超前 / 落后的消息可能缓存或重排
typescript
let lastSeq = 0
function handleMessage(msg) {
if (msg.seq <= lastSeq) return // 丢弃重复或落后
lastSeq = msg.seq
process(msg)
}
④、微小示例
typescript
class ReliableWS {
ws: WebSocket
pending: Map<string, any> = new Map()
receivedIds: Set<string> = new Set()
constructor(url: string) {
this.ws = new WebSocket(url)
this.ws.onmessage = (e) => this.onMessage(JSON.parse(e.data))
}
send(msg: any) {
this.pending.set(msg.msgId, msg)
this.ws.send(JSON.stringify(msg))
}
onMessage(msg: any) {
// 处理 ACK
if (msg.type === "ACK") {
this.pending.delete(msg.ackMsgId)
return
}
// 去重
if (this.receivedIds.has(msg.msgId)) return
this.receivedIds.add(msg.msgId)
// 顺序处理(简单示例)
this.process(msg)
// 返回 ACK
this.ws.send(JSON.stringify({ type: "ACK", ackMsgId: msg.msgId }))
}
process(msg: any) {
console.log("处理消息:", msg)
}
}
(6)、消息分发与消费模型
前端 WebSocket 的消息分发与消费模型,就是"把 WebSocket 收到的消息通过总线路由到各模块订阅者,实现多模块解耦、可扩展和可靠消费"。
在前端 WebSocket SDK / Service 中,消息不能直接丢给 UI,否则会出现:
- UI 逻辑耦合 WebSocket
- 多个模块竞争同一条消息
- 难以处理不同类型消息
- 消息丢失或重复处理
所以需要一个分层设计:
typescript
WebSocket Connection
↓
消息总线 / Dispatcher
↓
模块 / 组件订阅消息
核心思想:
- 消息路由
- 根据消息类型或 topic 分发给不同模块
- 类似"事件类型映射表"
- 订阅 / 发布
- 模块主动订阅自己关心的消息类型
- WebSocket 收到消息后发布到总线
- 多模块消费同一连接消息
- 一个 WebSocket 连接可以服务多个模块
- 每个模块只关心自己订阅的消息
微小示例
假设我们的 WebSocket 收到不同类型的消息:
typescript
interface WSMessage {
type: string
payload: any
}
①、创建一个简单消息总线(Dispatcher)
typescript
type Callback = (msg: WSMessage) => void
class MessageBus {
private handlers: Record<string, Callback[]> = {}
subscribe(type: string, cb: Callback) {
if (!this.handlers[type]) this.handlers[type] = []
this.handlers[type].push(cb)
return () => this.unsubscribe(type, cb)
}
unsubscribe(type: string, cb: Callback) {
this.handlers[type] = (this.handlers[type] || []).filter(h => h !== cb)
}
publish(msg: WSMessage) {
(this.handlers[msg.type] || []).forEach(cb => cb(msg))
}
}
②、WebSocket 封装
typescript
class WSClient {
private ws: WebSocket
private bus = new MessageBus()
constructor(url: string) {
this.ws = new WebSocket(url)
this.ws.onmessage = (e) => {
const msg: WSMessage = JSON.parse(e.data)
this.bus.publish(msg) // 核心:总线分发
}
}
subscribe(type: string, cb: Callback) {
return this.bus.subscribe(type, cb)
}
send(msg: WSMessage) {
this.ws.send(JSON.stringify(msg))
}
}
③、多模块消费示例
typescript
const client = new WSClient('wss://example.com/ws')
// 模块 A 关心聊天消息
client.subscribe('CHAT', (msg) => {
console.log('模块 A 收到聊天消息:', msg.payload)
})
// 模块 B 关心订单状态
client.subscribe('ORDER_STATUS', (msg) => {
console.log('模块 B 收到订单消息:', msg.payload)
})
✅ 说明:
- 一个 WebSocket 连接
- 多个模块可以独立订阅消息
- WebSocket 与模块 UI 完全解耦
- 消息类型路由 + Pub/Sub 完整实现
(7)、架构解耦与职责分离
架构解耦与职责分离 = 把 WebSocket 的连接管理、消息总线、业务接口、UI 消费层彻底拆开,让每一层只负责自己的职责,实现可复用、可测试、多模块共享、低耦合的系统。
定义:
- 架构解耦 = WebSocket 与 UI / 页面业务逻辑彻底分离
- 职责分离 = 每个模块只关心自己的消息,不直接操作连接
核心目标:
- 消息逻辑与 UI 无耦合 → 页面刷新 / 模块拆分不影响连接
- 可测试 / 可扩展 → SDK 层统一处理连接、协议、重连、ACK
- 支持多模块共享同一连接 → 节省资源、便于管理
如果不解耦,常见问题:
| 问题 | 后果 |
|---|---|
UI 直接用 ws.onmessage |
模块逻辑耦合,难复用、难维护 |
| 每个模块创建独立连接 | 资源浪费,心跳 / 重连难管理 |
| 消息处理混乱 | ACK / 去重难实现,顺序可能错乱 |
架构解耦与职责分离示例:
①、封装 WebSocket 客户端
- 负责:连接、重连、心跳、消息收发
- 不做业务处理,只做基础功能
typescript
class WSConnection {
ws: WebSocket
onMessage?: (msg: any) => void
constructor(url: string) {
this.ws = new WebSocket(url)
this.ws.onmessage = e => this.onMessage && this.onMessage(JSON.parse(e.data))
}
send(msg: any) { this.ws.send(JSON.stringify(msg)) }
}
②、消息总线 / Dispatcher
- 负责:消息路由、订阅 / 发布
- 让多个模块独立消费消息
typescript
type Callback = (msg: any) => void
class MessageBus {
private handlers: Record<string, Callback[]> = {}
subscribe(type: string, cb: Callback) {
if (!this.handlers[type]) this.handlers[type] = []
this.handlers[type].push(cb)
return () => this.unsubscribe(type, cb)
}
unsubscribe(type: string, cb: Callback) {
this.handlers[type] = (this.handlers[type] || []).filter(h => h !== cb)
}
publish(msg: any) {
(this.handlers[msg.type] || []).forEach(cb => cb(msg))
}
}
③、SDK / Service 层(业务层接口)
- 负责:提供模块调用接口
- 不关心 WebSocket 底层实现
- 可以组合 可靠性 + 消息协议 + 分发
typescript
class WSClient {
private conn: WSConnection
private bus = new MessageBus()
constructor(url: string) {
this.conn = new WSConnection(url)
this.conn.onMessage = msg => this.bus.publish(msg)
}
send(msg: any) { this.conn.send(msg) }
subscribe(type: string, cb: Callback) { return this.bus.subscribe(type, cb) }
}
④、UI / 业务层(消费层)
typescript
const client = new WSClient("wss://example.com/ws")
// 聊天模块订阅聊天消息
client.subscribe("CHAT", msg => console.log("聊天模块收到:", msg.payload))
// 订单模块订阅订单状态
client.subscribe("ORDER_STATUS", msg => console.log("订单模块收到:", msg.payload))
特点:
- UI 与连接解耦
- 多模块共享同一连接
- 模块只关心自己消息
- SDK 可复用 / 可测试
⑤、额外加分点
- 统一管理心跳 / 重连 / ACK → 模块无需关心
- 支持动态订阅 / 退订 → 页面路由切换安全
- 可扩展协议版本 → 新协议只修改 SDK
- 支持消息分发策略 → topic、filter、priority
4、一个可复用的 WebSocket Client(简化示例)
typescript
class WSClient {
ws?: WebSocket
retry = 0
connect(url: string) {
this.ws = new WebSocket(url)
this.ws.onopen = () => this.retry = 0
this.ws.onclose = () => {
setTimeout(() => this.connect(url), Math.min(2 ** this.retry * 1000, 30_000))
this.retry++
}
}
send(data: any) {
if (this.ws?.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify(data))
}
}
}
5、前端 WebSocket 常见翻车点(血泪)
❌ 一个页面多个 WebSocket
❌ 不关心 readyState
❌ 没有心跳
❌ 切后台连接被杀
❌ 消息没 version
五、服务端 WebSocket 架构要点
1、单机 WebSocket ≠ 可用系统
单机问题:
- 连接数有限
- 重启即断
- 无法横向扩展
2、标准 WebSocket 集群架构
typescript
Client
↓
LB(4 层 / 7 层)
↓
WS Server
↓
Message Broker(Redis / Kafka)
📌 关键点:
- 连接在 WS Server
- 消息在 Broker
3、Session 粘性(Sticky Session)
| 方式 | 是否必须 |
|---|---|
| WebSocket | ✅ |
| SSE | ❌ |
六、认清 WebSocket 和 SSE
1、WebSocket 和 SSE对比
- WebSocket 是"双向实时通信通道"
- SSE 是"服务端推送事件流"
| 维度 | SSE | WebSocket |
|---|---|---|
| 协议基础 | HTTP | HTTP Upgrade → 独立协议 |
| 通信方向 | 单向(Server → Client) | 双向(全双工) |
| 连接类型 | HTTP 长连接 | TCP 长连接 |
| 自动重连 | ✅ 内建 | ❌ 手写 |
| 心跳 | ❌ 不需要 | ✅ 必须 |
| 顺序 / 断点续传 | ✅ Last-Event-ID | ❌ 自己实现 |
2、最容易选错的 6 个场景(血泪)
❌ 用 WebSocket,其实该用 SSE:
| 场景 | 正确选择 |
|---|---|
| 订单状态变化 | SSE |
| 支付结果回调 | SSE |
| AI 流式输出 | SSE |
| 构建 / 导入进度 | SSE |
| 日志 / 监控流 | SSE |
| 消息通知 / 红点 | SSE |
✅ 必须 WebSocket 的场景:
| 场景 | 原因 |
|---|---|
| IM 聊天 | 双向交互 |
| 实时协作 | 双向状态同步 |
| 游戏 / 对战 | 低延迟双向 |
| 设备实时控制 | 指令下发 |
七、认清「HTTP、WebSocket」和 「Socket」
1、先给你一张「总关系图」(先立世界观)
typescript
┌─────────────┐
│ HTTP │ ← 应用层协议
├─────────────┤
│ WebSocket │ ← 应用层协议(可升级)
├─────────────┤
│ Socket │ ← 通信抽象(基于 TCP / UDP)
├─────────────┤
│ TCP/UDP │ ← 传输层
├─────────────┤
│ IP │
└─────────────┘
- HTTP / WebSocket 是"协议" → 【协议】
- Socket 是"通信能力 / API 抽象"→【API】
2、HTTP 与 WebSocket 对比

| 对比维度 | HTTP | WebSocket |
|---|---|---|
| 本质 | 请求-响应的应用层协议 | 基于 TCP 的全双工应用层协议(初始通过 HTTP Upgrade 握手) |
| 通信模式 | 单向:客户端发起请求 → 服务端响应 | 双向全双工:客户端和服务端都可以主动发送消息 |
| 连接特性 | 短连接为主(HTTP/1.0 默认),HTTP/1.1 可 Keep-Alive | 长连接(TCP 连接保持,直至关闭) |
| 握手过程 | TCP 三次握手 + HTTP 请求 | TCP 三次握手 + HTTP Upgrade 握手 → WebSocket 协议 |
| 数据传输格式 | 文本:HTTP Header + Body(JSON、HTML、文件等) | 帧(Frame):文本帧 / 二进制帧,开销小 |
| 服务端主动推送 | ❌ 不支持(客户端必须请求) | ✅ 支持,服务端可主动发送消息 |
| 延迟 | 较高,每次请求需发 HTTP Header | 低,建立连接后数据直接发送帧 |
| 浏览器原生支持 | ✅ XMLHttpRequest / Fetch / Fetch Stream | ✅ WebSocket API |
| 心跳 / 保活 | 依赖 TCP Keep-Alive 或轮询实现 | 需要手动实现心跳 / Ping-Pong |
| 扩展性 | 易缓存 / 代理友好 / CDN 支持 | 粘性会话 + Broker / 消息总线,扩展稍复杂 |
| 典型场景 | API 调用、页面资源加载、REST 接口、文件下载 | 实时聊天、协作编辑、IoT 控制、游戏实时交互、实时通知 |
| 优点 | 简单、普遍兼容、易缓存、HTTP/2 支持多路复用 | 双向、低延迟、轻量帧结构、实时性强 |
| 缺点 | 无法主动推送,开销大,高频场景不适合 | 服务端复杂度高,需要心跳/重连/扩展支持,浏览器每 tab 一个连接 |
- HTTP = 请求-响应,适合查询 / API / 静态资源
- WebSocket = 双向实时通道,适合低延迟、高频交互
八、WebSocket 在典型系统中的位置
1、IM 系统(时通讯系统)------人与人、人与群实时交流的系统
- WebSocket:消息通道
- HTTP:历史消息 / 登录
2、IoT 系统(物联网系统)------"管理、监控、控制大量设备"的系统
- WebSocket:控制 / 实时状态
- MQTT:设备通信
3、AI 实时系统------AI 推理结果"边算边给用户"的系统
- SSE:流式输出
- WebSocket:多 Agent 协作
九、WebSocket 的致命坑(真实血泪)
❌ 误区 1:连接越多越好
- 实际:FD、内存、心跳成本爆炸
❌ 误区 2:不做 ACK
- 实际:消息丢了你都不知道
❌ 误区 3:忽略移动端
- 实际:后台直接断连
❌ 误区 4:用 WebSocket 做"状态同步"
- 实际:状态错乱地狱