WebSocket
面试一句话总结:WebSocket 是一种基于 TCP 的全双工、双向通信协议。它借用 HTTP 协议完成一次握手升级(状态码 101),之后便摆脱 HTTP 的束缚,以极低开销的二进制帧进行实时数据传输。常用于聊天室、协同编辑和实时大屏。
1. 核心工作原理
WebSocket 虽然名字里带 Web,但它实际上是一个独立的协议(ws:// 或加密的 wss://),它只是在“建立连接”这个阶段“蹭”了一下 HTTP 的车。
① 协议升级 (HTTP 握手)
建立连接时,客户端会发送一个标准的 HTTP GET 请求,并在请求头中要求“升级协议”:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
服务端如果支持 WebSocket,会返回 101 Switching Protocols 状态码,并计算一个 Sec-WebSocket-Accept 返回给客户端。
至此,HTTP 功成身退,这条 TCP 连接被 WebSocket 正式接管。
② 数据传输格式 (帧 Frame)
与 HTTP 每次请求都携带几百字节的臃肿 Header 不同,WebSocket 传输数据时使用的是帧 (Frame) 格式,帧头极小(通常只有 2~10 个字节)。
它原生支持传输文本(Text)以及二进制数据(ArrayBuffer 和 Blob),非常适合音视频流传输。
2. 与 HTTP 及 SSE 的对比 (高频考点)
在高级面试中,面试官往往会让你对比这几种通信方案的选型。
| 特性 | HTTP (轮询/长轮询) | SSE (Server-Sent Events) | WebSocket |
|---|---|---|---|
| 通信方向 | 半双工 (单向) | 单向 (服务端推给客户端) | 全双工 (双向) |
| 协议层 | HTTP | HTTP | WebSocket (独立协议) |
| 重连机制 | 无 | 浏览器原生支持自动重连 | 需手写心跳保活和断线重连 |
| 头部开销 | 极大 (每次带完整 Header) | 较大 | 极小 (2~10 字节帧头) |
| 适用场景 | 低频数据请求 | AI 打字机、股票大屏、消息通知 | IM 聊天、协同编辑、在线游戏 |
实战选型思考: 如果业务需求只是“服务端向客户端单向推数据”(如 ChatGPT 的回复、系统广播),绝对优先选择 SSE。它更轻量,且原生支持断线重连。只有在需要高频、实时的双向交互时,才动用 WebSocket 这个重型武器。
3. 工程实战与难点解决
如果在简历里写了 WebSocket,面试官一定会问你如何保证连接的稳定性。
① 心跳检测 (Ping/Pong 保活)
痛点:TCP 长连接在长时间没有数据传输时,很容易被中间的网络设备(如 Nginx、防火墙、运营商 NAT)当作死连接而偷偷掐断,此时前端根本感知不到(触发不了 onclose)。
解法:前端需要实现心跳机制。每隔固定的时间(如 30 秒)向服务端发送一个内容为 Ping 的空帧,服务端收到后立刻回复 Pong。如果前端连续几次发了 Ping 却没有收到 Pong,就认为连接已死,主动执行重连。
② 优雅的断线重连 (指数退避算法)
痛点:当服务器宕机或网络抖动导致连接断开时(触发 onclose / onerror),如果几万个客户端同时疯狂重试,会导致服务器瞬间被打挂(类似 DDoS 攻击)。
解法:使用指数退避算法(Exponential Backoff)。重试间隔不是固定的,而是成倍增加(如 1s, 2s, 4s, 8s, 16s...),并且还要加上一段随机抖动时间 (Jitter),打散客户端的重连请求峰值。
③ 身份鉴权问题
痛点:WebSocket 无法像 HTTP 请求那样,利用拦截器在 Header 里方便地带上 Authorization: Bearer Token。
解法:
- URL 传参(最常见但有安全隐患):
ws://api.com?token=xxx。缺点是 Token 容易记录在网关的访问日志中。 - 连接后第一条消息鉴权(最安全):建立连接后,服务端不接受任何业务指令,要求前端先发一条专门的
{"type": "auth", "token": "xxx"}消息。服务端校验通过后才放行,否则直接掐断连接。
4. 安全防护 (WSS)
- WSS 协议:生产环境必须使用
wss://代替ws://。它在 TLS 层上进行了加密(类似于 HTTPS),能有效防止抓包和中间人攻击。 - 跨站 WebSocket 劫持 (CSWSH):类似于 HTTP 的 CSRF 攻击。恶意网站可以通过 JS 直接向你的服务端发起 WebSocket 连接。防范方案:服务端在握手阶段,必须严格校验 HTTP 请求头中的
Origin字段是否在白名单内。
5. 面试题: 项目中如何设计 WebSocket 通信?
面试官考察的不是"WebSocket 是什么",而是如何把它封装成一个工业级模块。回答时围绕"分层、解耦、可靠性"三个关键词展开即可。
① 整体架构 (五层模型)
设计意图:把"连接管理"和"业务逻辑"彻底解耦。WebSocket 只负责收发字节流,业务层只关心
type和payload,完全不碰原生 API。
┌────────────────────────────────┐
│ 业务层 (组件/Store) │ ← on('chat:msg', handler)
├────────────────────────────────┤
│ 事件总线 (EventEmitter) │ ← 按 type 派发
├────────────────────────────────┤
│ 协议层 (Message Codec) │ ← JSON 序列化 + msgId 去重
├────────────────────────────────┤
│ 连接管理 (Socket Manager) │ ← 单例 + 心跳 + 退避重连
├────────────────────────────────┤
│ WebSocket 原生 API │
└────────────────────────────────┘
② 自定义消息协议 (核心考点)
WebSocket 协议只定义"帧 (Frame)",不定义"消息 (Message)"的语义。所以必须在业务层定义自己的格式:
// 为什么必须有 msgId / timestamp?
// → 实现 ACK 应答、消息去重、断点续传、超时重发的基石
interface WsMessage<T = unknown> {
type: string; // 业务类型: 'chat' | 'auth' | 'ack' | 'ping' ...
msgId: string; // 唯一 ID (用 crypto.randomUUID())
timestamp: number; // 发送时间戳,用于排序 + 超时判断
payload: T; // 业务数据
}
反例:直接把业务对象
JSON.stringify扔进send(),不带任何元信息。这种写法在面试中直接暴露"没做过真实项目"。
③ 连接管理: 单例 + 状态机
一个应用绝对只允许一个 WebSocket 实例。多实例会引发鉴权冲突、消息重复到达、内存泄漏三大问题。
[CONNECTING] ──onopen──▶ [OPEN] ──onclose/error──▶ [CLOSED]
│ │
│ 触发重连 (指数退避) │
▼ ▼
[RECONNECTING] ◀──────────────┘
④ 核心代码骨架 (30 行精华版)
class WsClient {
private ws: WebSocket | null = null;
private bus = new EventEmitter();
private queue: WsMessage[] = []; // 待发送队列:断线期间消息暂存
send<T>(type: string, payload: T) {
const msg: WsMessage<T> = {
type,
payload,
msgId: crypto.randomUUID(),
timestamp: Date.now(),
};
// 连接未就绪时入队,重连后 flush,避免消息丢失
this.ws?.readyState === WebSocket.OPEN
? this.ws.send(JSON.stringify(msg))
: this.queue.push(msg);
}
on(type: string, handler: (p: any) => void) {
this.bus.on(type, handler);
}
private onMessage(e: MessageEvent) {
const msg = JSON.parse(e.data);
// 协议层拦截心跳/ACK,绝不抛到业务层
if (msg.type === "pong" || msg.type === "ack") return;
this.bus.emit(msg.type, msg.payload);
}
}
⑤ 回答模板 (直接背)
"项目里我们这样设计 WebSocket":
- 连接层:单例管理 + 状态机 + 30s 心跳 + 指数退避重连 (1s→2s→4s,封顶 30s,加随机抖动)。
- 协议层:统一封装为
{ type, msgId, timestamp, payload },业务按type订阅,完全解耦。- 可靠性:关键消息 (支付、指令) 走 ACK 应答,客户端按
msgId去重,断线时消息暂存到队列,重连后 flush。- 多端:用户在新设备登录时,服务端主动
close旧连接,避免消息错乱 (类似微信的"被踢下线")。- 安全:生产用
wss://,服务端校验Origin白名单防 CSWSH。