文章

WebSocket通信机制深度解析

WebSocket 协议原理、帧格式、心跳保活、断线重连策略,面试必问。

WebSocket通信机制深度解析

一句话概括

WebSocket 通过 HTTP 101 升级握手建连,之后在单个 TCP 连接上实现全双工通信——服务端可以主动推消息给客户端,帧头最小 2 字节,是即时通讯、在线协作、实时行情等场景的基础协议。

核心知识点

1. 从 HTTP 升级到 WebSocket

WebSocket 连接以 HTTP 请求开头,服务器同意后切协议:

1
2
3
4
5
6
7
8
9
Client → Server:  GET /chat HTTP/1.1
                  Upgrade: websocket
                  Connection: Upgrade
                  Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==

Server → Client:  HTTP/1.1 101 Switching Protocols
                  Upgrade: websocket
                  Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
// ↑ 之后这条 TCP 连接进入 WebSocket 模式,双向收发消息帧

Sec-WebSocket-Accept 防止误升级——服务器把客户端 Key 拼上魔数 GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11 做 SHA1,确保这是一个有意发起的 WebSocket 请求而非误触。

1
2
3
const crypto = require('node:crypto');
const GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';
const accept = crypto.createHash('sha1').update(key + GUID).digest('base64');

2. WebSocket 帧结构:最小 2 字节

1
2
3
4
5
6
7
8
 0  1  2  3  4  5  6  7  8  9 ...
+--+--+--+--+--+--+--+--+--+--+--
|F|R|R|R| opcode |M| payload len|
|I|S|S|S|  (4)   |A|   (7)      |
|N|V|V|V|        |S|            |
+--+--+--+--+--+--+--+--+--+--+--
FIN=1 表示最后一帧;opcode: 1=文本, 2=二进制, 8=关闭, 9=ping, 10=pong
MASK=1 表示客户端→服务端时载荷被掩码(防缓存污染攻击)
  • 客户端发服务端必须掩码(随机 4 字节 XOR 载荷)
  • 服务端发客户端不掩码
  • 掩码开销 4 字节 + XOR 运算,性能影响可忽略

3. 心跳保活:ping/pong

TCP 长连接可能被中间代理/NAT/防火墙静默断开(通常 60-120 秒无数据就关)。WebSocket 协议内置 ping/pong 帧(opcode 9/10),不占用数据通道:

1
2
3
4
5
6
7
8
9
10
// 服务端每 30 秒发 ping,客户端自动回 pong
const ws = new WebSocket('wss://example.com');
setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) ws.send('ping'); // 应用层心跳
  // 也可用 ws.ping()(部分库支持,发 opcode=9 协议帧)
}, 30000);

// 超时检测:如果连续两次心跳没收到 pong,主动断开重连
let missedPongs = 0;
ws.on('pong', () => { missedPongs = 0; });

注意:浏览器原生 WebSocket API 不暴露 ping/pong 帧接收,通常做法是应用层自定义心跳消息(JSON {"type":"ping"}),服务端检测超时。

4. 断线重连策略

WebSocket 连接不是永远的——网络切换、服务端重启、中间代理超时都会断。成熟的方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class RobustWS {
  constructor(url, { maxRetries = 10, baseDelay = 1000 } = {}) {
    this.url = url;
    this.retries = 0;
    this.baseDelay = baseDelay;
    this.maxRetries = maxRetries;
    this.connect();
  }

  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onclose = (e) => {
      if (this.retries < this.maxRetries) {
        const delay = Math.min(this.baseDelay * 2 ** this.retries, 30000);
        setTimeout(() => { this.retries++; this.connect(); }, delay);
      }
    };
    this.ws.onopen = () => { this.retries = 0; };
  }
}

指数退避 + 上限 30 秒是业界标准做法。更完善的要加上随机抖动避免惊群。

5. 为什么 WebSocket 比轮询高效

 短轮询长轮询WebSocket
消息延迟轮询间隔(秒级)100-300ms< 1ms
每秒 1000 条消息的请求数1000 次 HTTP~500 次 HTTP0 次(纯帧)
单条消息网络开销800B 头 + 负载800B 头 + 负载2-6B 帧头 + 负载
服务端主动推送不支持变通支持原生支持

其实你每天都在用

  • 微信网页版/钉钉 Web:消息列表实时更新走的 WebSocket,你发消息和收消息是同一条 TCP 连接
  • VS Code Live Share / Figma 协作:多人同时编辑,每个人的光标位置通过 WebSocket 广播
  • 币圈/K 线图:BTC 价格每秒变化,WebSocket 实时推送 tick 数据
  • Webpack HMR / Vite HMR:开发时代码一存盘浏览器自动更新,底层也是 WebSocket
  • Chrome DevTools Remote Debugging:用 WebSocket 在 PC 上调试手机上打开的页面

常见误解(FAQ)

❌ 误区一:”WebSocket 连接一直开着就是长轮询的升级版”

长轮询每次还是要完整的 HTTP 请求-响应周期,有 HTTP header 开销。WebSocket 握手后就进入帧模式,最小帧头 2 字节,且服务端可以主动推不需要客户端先问——这是本质区别。

❌ 误区二:”WebSocket 不需要心跳”

TCP 建立的连接不等同于永远通——NAT 映射、负载均衡器、代理都可能静默断开空闲连接。业界标准是 30-60 秒心跳间隔,连续超时 2-3 次判定断开。没有心跳的 WebSocket 连接就是在”装活”。

❌ 误区三:”WebSocket 一定比 HTTP/2 快”

HTTP/2 也有服务端推送(Server Push)和多路复用,对于低频的实时同步场景(如通知推送),SSE over HTTP/2 可能更简单可靠。WebSocket 的优势在高频率、双向的场景。

❌ 误区四:”wss:// 就是 ws:// 加 TLS 那么简单”

wss:// 确实 = ws:// over TLS,但关键区别在于:ws 握手是明文 HTTP Upgrade,中间代理可能拦截或破坏升级头导致失败。wss 在 TLS 隧道内升级,代理看不到 Upgrade 头,兼容性好得多。生产环境必须用 wss,别用 ws。

一句话总结

WebSocket 把 HTTP 的单次问答模式变成了持久对话——握手只做一次,之后双向自由收发帧,配上心跳 + 指数退避重连 + wss 加密,就是生产级实时通信基础设施。

本文由作者按照 CC BY 4.0 进行授权