WebSocket 实现原理详解

从协议规范 (RFC 6455) 到 OS 内核 Socket,一次性讲清楚

1. 为什么需要 WebSocket

HTTP 的"半双工"困境

传统 HTTP 协议是请求-响应模型:客户端发请求,服务端回响应。服务端不能主动推送数据给客户端。这就催生了各种"不优雅"的变通方案:

方案原理问题
短轮询 (Polling) 客户端定时发 HTTP 请求查询新数据 大量无效请求、高延迟、浪费带宽
长轮询 (Long Polling) 客户端请求后,服务端 hold 住直到有数据才响应 连接超时处理复杂、每个消息仍需完整 HTTP 头
HTTP Streaming 服务端持续向响应流写入数据(分块传输) 代理/防火墙兼容性差、单向(仅服务端推送)
WebSocket 的解决方案:在单个 TCP 连接上实现全双工、低延迟、低开销的双向通信。一次握手后,双方可以随时发送数据,帧头仅 2-10 字节。

WebSocket 协议栈位置

┌─────────────────────────────┐
│       应用层 (Application)    │  ← WebSocket 协议 (RFC 6455)
├─────────────────────────────┤
│       传输层 (Transport)      │  ← TCP
├─────────────────────────────┤
│       网络层 (Internet)       │  ← IP
├─────────────────────────────┤
│       链路层 (Link)           │  ← Ethernet / Wi-Fi
└─────────────────────────────┘

WebSocket 位于 OSI 模型的应用层,但它建立在 TCP 之上,与 HTTP 同级 — 只是借用了 HTTP 的 80 / 443 端口来完成初始握手。

2. HTTP 升级握手 (Opening Handshake)

WebSocket 连接的生命始于一次特殊的 HTTP 请求。客户端通过 HTTP Upgrade 机制,将普通 HTTP 连接"升级"为 WebSocket 连接。

协议升级过程

💻
客户端 Client
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHN...
Sec-WebSocket-Version: 13
🖥️
服务端 Server
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept:
s3pPLMB...

关键字段逐一解析

字段方向含义
Upgrade: websocket 双向 请求升级为 WebSocket 协议
Connection: Upgrade 双向 告知对方这是一个升级请求(HTTP/1.1 必须是 hop-by-hop 头)
Sec-WebSocket-Key C → S 客户端生成的 16 字节随机数,经 Base64 编码。防止缓存代理误接 WebSocket 连接
Sec-WebSocket-Accept S → C 服务端对 Key 的确认值。计算方式:Base64(SHA1(Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))
Sec-WebSocket-Version C → S 协议版本,当前为 13
Sec-WebSocket-Protocol 可选 子协议协商,如 chat, soap, wamp
Sec-WebSocket-Extensions 可选 扩展协商,如 permessage-deflate(压缩)

Sec-WebSocket-Accept 的计算过程

伪代码
// 1. 客户端生成随机 16 字节
key = random_bytes(16)           // 例如: [0xdG, 0xhl, 0xIH, ...]
key_b64 = base64_encode(key)     //  → "dGhlIHNhbXBsZSBub25jZQ=="

// 2. 服务端拼接魔术字符串后 SHA1
magic = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"
accept = base64_encode(sha1(key_b64 + magic))
//  → "s3pPLMBiTxaQ9kYGzzhZRbK+xOo="

// 3. 客户端本地验证:用同样的 key 计算,对比 Accept 值
//    一致 → 握手成功,不一致 → 断开连接
魔术字符串的作用:258EAFA5-E914-47DA-95CA-C5AB0DC85B11 是一个全球唯一的 GUID,确保即使客户端 Key 被攻击者猜到,也无法伪造 Accept 值(因为攻击者不知道服务端会做 SHA1 拼接)。同时它让 WebSocket 握手与普通 HTTP 响应在语义上完全不同,避免与 HTTP 中间件冲突。

握手成功后,TCP 连接不再走 HTTP 协议,之后所有数据都按 WebSocket 帧格式传输。

3. 帧协议 (Frame Protocol) — 核心

握手完成后,所有通信数据都被封装成帧 (Frame)。帧是 WebSocket 的最小传输单元。

3.1 帧格式完整图解

字节偏移 →
Byte 0 FIN RSV1 RSV2 ← 第 0 字节 →
Byte 1 MASK Payl ← 第 1 字节 →
Byte 2-9 Extended Payload Length (0 / 2 / 8 字节,由 Payload Len 决定)
Byte ? Masking-Key (0 或 4 字节,仅客户端→服务端帧有)
Byte ? Payload Data (实际数据)

3.2 逐字段详解

字段位数说明
FIN1 bit是否为消息的最后一个分片帧。1 = 最后一帧
RSV1/2/33 bit保留位,用于扩展(如压缩标记)。通常为 0
Opcode4 bit帧类型,见下方表格
MASK1 bitPayload 是否经过掩码处理。客户端→服务端必须为 1,服务端→客户端必须为 0
Payload len7 bit载荷长度:0-125=实际长度;126=后续 2 字节是长度;127=后续 8 字节是长度
Masking-Key0/32 bit当 MASK=1 时出现,4 字节随机密钥,用于对 Payload 做 XOR 解码
Payload Data变长实际传输的数据(文本 UTF-8、二进制等)

3.3 Opcode(操作码)一览

Opcode含义方向说明
0x0Continuation双向分片消息的后续帧
0x1Text双向文本帧(UTF-8 编码)
0x2Binary双向二进制帧
0x8Close双向关闭连接
0x9Ping双向心跳探测
0xAPong双向心跳响应
0x3-0x7保留,用于将来的非控制帧
0xB-0xF保留,用于将来的控制帧

3.4 掩码 (Masking) — 防止缓存投毒攻击

客户端发往服务端的每一帧必须加掩码。原因:2011 年安全研究者发现,如果浏览器发送的 WebSocket 帧不加密,恶意代理可能将帧内容解释为 HTTP 请求,造成缓存投毒 (Cache Poisoning)

掩码算法 (客户端编码 / 服务端解码)
// 客户端:对 Payload 做 XOR
masking_key = random_4_bytes()
for i in range(len(payload)):
    payload[i] = payload[i] ^ masking_key[i % 4]

// 服务端:用同样的 key 再做一次 XOR 即恢复原文
// 因为 A ^ K ^ K = A
for i in range(len(payload)):
    payload[i] = payload[i] ^ masking_key[i % 4]
为什么 XOR 是正确的? XOR 满足 A ⊕ K ⊕ K = A(自逆性),所以编码和解码使用完全相同的算法。同时每次帧的掩码密钥随机生成,即便是相同的 Payload,每次传输的密文也完全不同,彻底杜绝了缓存投毒的可能性。

3.5 分片 (Fragmentation)

当一条消息过大时,可以拆分成多个帧发送:

帧 1:  FIN=0  Opcode=Text   Payload="Hello, this is a ve"
帧 2:  FIN=0  Opcode=0x0    Payload="ry long message that spa"
帧 3:  FIN=1  Opcode=0x0    Payload="ns multiple frames."

分片允许流式发送——不需要在发送前就知道整个消息的大小。这对于实时转码、大文件传输等场景非常有用。

4. 连接生命周期

CONNECTING (0) — TCP 三次握手

new WebSocket("ws://...") 触发 DNS 解析 + TCP SYN → SYN-ACK → ACK,建立底层 TCP 连接。

OPENING — HTTP Upgrade 握手

客户端发送 Upgrade 请求,服务端返回 101。握手成功后,readyState 变为 OPEN。

OPEN (1) — 数据传输

双方通过帧协议自由收发数据。可以同时进行(全双工)。onmessage 事件持续触发。

CLOSING (2) — 关闭握手

任一方发送 Close 帧 (Opcode 0x8),另一方回复 Close 帧确认。Close 帧可包含 2 字节状态码 + 原因文本。

CLOSED (3) — TCP 四次挥手

底层 TCP 连接关闭,释放端口资源。onclose 事件触发。

Close 帧状态码

状态码含义
1000正常关闭
1001端点离开(如页面关闭、服务器关闭)
1002协议错误
1003收到不支持的数据类型
1006连接异常关闭(无 Close 帧就断开)
1007数据格式不合法(如非 UTF-8 文本)
1008策略冲突
1009消息过大
1011服务端内部错误
1012服务重启

5. 全双工通信 — 它是如何做到的

核心:TCP 本身就是全双工的

TCP 连接在建立后,双方可以同时、独立地发送数据。每个方向都有自己的序列号 (SEQ) 和确认号 (ACK),互不干扰。


  客户端                                     服务端
    │                                          │
    │ ─── SEQ=1001 "Hello" ──────────────→     │
    │ ←──────── SEQ=5001 "Hi" ──────────────── │
    │                                          │
    │     ← 两条消息可以同时在链路上传输 →        │
    │     (不同方向,互不阻塞)                    │
    │                                          │
    │ ─── SEQ=1007 "World" ───────────────→     │
    │ ←──────── SEQ=5004 "Bye" ──────────────── │

WebSocket 做了什么额外工作?

WebSocket 在 TCP 之上加了帧边界

1
TCP 流 → 帧切分:TCP 是字节流,没有消息边界。WebSocket 帧头的 Payload Length 字段告诉接收方「这一帧的结束位置在哪里」。
2
帧 → 消息组装:FIN 位 + Continuation 帧机制让接收方知道「这一条完整消息的边界在哪里」,支持流式处理和分片重组。
3
消息 → 应用回调:组装完整的消息后,触发 onmessage(msg) 回调,交给上层应用处理。
关键理解:WebSocket 全双工的"魔法"不在协议本身,而在 TCP。WebSocket 只是给 TCP 字节流加了结构(帧格式),让应用程序能方便地区分每条消息的边界。底层依然是 TCP 的双向字节通道。

6. 方案全面对比

特性短轮询长轮询SSEWebSocket
通信方向单向(请求-响应)单向单向(S→C)双向全双工
协议HTTPHTTPHTTPWebSocket (ws/wss)
头部开销大(每次完整 HTTP 头)极小(2-10 字节帧头)
实时性极好
服务端推送模拟模拟原生支持原生支持
二进制支持需编码需编码仅文本原生二进制帧
连接数极多极少(复用单连接)
浏览器兼容全部全部除 IE 外全部所有现代浏览器
自动重连内置需自行实现
代理/防火墙友好WSS 是(加密后代理不可见)
SSE (Server-Sent Events) 仍然是 WebSocket 的有力补充:当你只需要服务端→客户端单向推送,且数据是纯文本时,SSE 更简单(基于 HTTP、自动重连、无需特殊服务器支持)。如果还需要客户端→服务端通信,则选 WebSocket。

7. 代码实现 — 从浏览器到服务器

7.1 浏览器端 (Client)

JavaScript (Browser)
// 创建连接
const ws = new WebSocket('wss://echo.websocket.org');

// 连接打开
ws.onopen = () => {
  console.log('✅ 连接已建立');
  ws.send('Hello Server!');           // 发送文本
  ws.send(new Uint8Array([1,2,3]));   // 发送二进制
};

// 接收消息
ws.onmessage = (event) => {
  console.log('📩 收到:', event.data);
  // event.data 类型: String (文本帧) 或 Blob/ArrayBuffer (二进制帧)
};

// 错误处理
ws.onerror = (err) => console.error('❌ 错误:', err);

// 连接关闭
ws.onclose = (event) => {
  console.log(`🔒 关闭: code=${event.code} reason=${event.reason}`);
};

// 主动关闭
ws.close(1000, '正常关闭');

7.2 Node.js 服务端 (原生实现思路)

Node.js (简化示意)
const crypto = require('crypto');
const net = require('net');

const MAGIC = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11';

const server = net.createServer(socket => {
  socket.once('data', data => {
    const req = data.toString();

    // 1. 解析 Sec-WebSocket-Key
    const keyMatch = req.match(/Sec-WebSocket-Key: (.+)\r\n/);
    if (!keyMatch) { socket.destroy(); return; }

    // 2. 计算 Accept
    const accept = crypto
      .createHash('sha1')
      .update(keyMatch[1].trim() + MAGIC)
      .digest('base64');

    // 3. 返回 101 响应
    socket.write(
      'HTTP/1.1 101 Switching Protocols\r\n' +
      'Upgrade: websocket\r\n' +
      'Connection: Upgrade\r\n' +
      `Sec-WebSocket-Accept: ${accept}\r\n` +
      '\r\n'
    );

    console.log('🤝 WebSocket 握手完成');
    // 此后走帧协议...
  });
});

server.listen(8080);

7.3 帧解析 (核心逻辑)

帧解码器
function decodeFrame(buffer) {
  const firstByte  = buffer[0];
  const secondByte = buffer[1];

  const FIN    = (firstByte & 0x80) >> 7;   // 第 0 位
  const opcode = firstByte & 0x0F;           // 低 4 位
  const MASK   = (secondByte & 0x80) >> 7;   // 第 0 位
  let   len    = secondByte & 0x7F;          // 低 7 位

  let offset = 2;

  // 扩展长度
  if (len === 126) {
    len = buffer.readUInt16BE(offset);
    offset += 2;
  } else if (len === 127) {
    // 注意:JavaScript 中 Number 只能安全表示到 2^53-1
    len = Number(buffer.readBigUInt64BE(offset));
    offset += 8;
  }

  // 掩码密钥
  let maskKey;
  if (MASK) {
    maskKey = buffer.slice(offset, offset + 4);
    offset += 4;
  }

  // 载荷数据
  let payload = buffer.slice(offset, offset + len);

  // XOR 解掩码
  if (MASK) {
    for (let i = 0; i < payload.length; i++) {
      payload[i] ^= maskKey[i % 4];
    }
  }

  return { FIN, opcode, payload };
}

8. 底层原理 — 从内核看 WebSocket

8.1 TCP Socket 基础

WebSocket 的底层就是一个普通的 TCP Socket。在 Linux 内核中,每个 TCP 连接由以下数据结构承载:

TCP 控制块 (TCB):包含本地 IP:Port、远端 IP:Port、发送缓冲区 (Send Buffer)、接收缓冲区 (Recv Buffer)、当前 SEQ 号、拥塞窗口 (cwnd)、重传超时 (RTO) 等。WebSocket 的消息传输本质上就是向这个 Socket 的发送缓冲区写数据,内核负责 TCP 分段、重传、拥塞控制。

8.2 服务端架构模式

模式原理适用规模
单线程 + epoll一个线程通过 epoll/kqueue 多路复用管理所有连接。C 语言高性能实现如 libwebsockets。数万连接
Event Loop (Node.js)单线程事件循环 + 非阻塞 I/O。ws 库底层使用 net.Socket,通过 libuv 与内核 epoll 交互。数千-数万连接
多线程/协程 (Go)goroutine 每连接模型,配合 Go netpoller(基于 epoll 的异步 I/O)。goroutine 在 I/O 等待时自动挂起。数十万连接
多进程 + 共享端口 (SO_REUSEPORT)多个进程绑定同一端口,内核将新连接均匀分发到各进程。Nginx 反向代理 + 后端 WebSocket 服务集群。百万级连接

8.3 数据流转全过程

浏览器 JS 调用:                             服务端处理链:
                                            
ws.send("hello")                             用户代码: conn.on('message', ...)
      │                                              ▲
      ▼                                              │
浏览器 WebSocket 实现                             帧解码器 (decodeFrame)
  · 编码为 Text 帧                                 · XOR 解掩码
  · MASK + XOR 掩码                                · 校验 UTF-8
  · 写入 TCP Send Buffer                           · 组装完整消息
      │                                              ▲
      ▼                                              │
┌─────────────┐                              ┌─────────────┐
│   内核 TCP   │  ── IP 网络 ──→             │   内核 TCP   │
│  · 分段     │                              │  · 接收重组   │
│  · 拥塞控制  │                              │  · ACK 确认   │
│  · 超时重传  │                              │  · 存入 Recv  │
└─────────────┘                              └─────────────┘
                                                    ▲
                                                    │
                                            epoll 通知: socket 可读
                                            → Event Loop 调度回调

8.4 内存与缓冲

每个 WebSocket 连接涉及的内存结构:

以百万连接为例:假设每连接用户态 10KB,内核态 200KB,总计约 210GB 内存。这也是为什么大规模 WebSocket 服务需要精细的 buffer 管理和连接池设计。

9. 安全与优化

9.1 WSS (WebSocket Secure)

wss:// 即 WebSocket over TLS。工作流程:

1
TCP 三次握手
2
TLS 握手(证书验证、密钥协商)
3
WebSocket Upgrade 握手(在加密通道内进行)
4
加密帧传输(TLS 记录的 payload 是 WebSocket 帧)
为什么 WSS 能穿透代理? 普通 WS 帧在透明代理眼中是"不明 TCP 数据",可能被拦截。WSS 将所有数据加密在 TLS 记录中,代理只能看到加密字节流,将其当作普通 HTTPS 流量放行。

9.2 心跳保活 (Ping/Pong)

服务端心跳示例
const HEARTBEAT_INTERVAL = 30000; // 30 秒

const interval = setInterval(() => {
  if (ws.readyState === WebSocket.OPEN) {
    ws.ping();  // 发送 Ping 帧 (Opcode 0x9)
  }
}, HEARTBEAT_INTERVAL);

ws.on('pong', () => {
  // 收到 Pong (Opcode 0xA),连接正常
  ws.isAlive = true;
});

Ping/Pong 是协议层面的控制帧(Opcode 0x9/0xA),不会被 onmessage 拦截。它们在帧头中明确区分于数据帧,可以穿插在正常数据流中发送。如果连续几次 Ping 没有 Pong 响应,即可判定连接已断开。

9.3 常见优化策略

策略说明
permessage-deflate消息级压缩扩展,对 Text 帧压缩率可达 60-80%。需在握手时协商
连接池客户端维护多个 ws 连接,负载均衡分发消息
消息合并将短时间内的多条小消息合并为一个 Binary 帧发送,减少帧头开销
自动重连 + 指数退避断开后延迟重连:1s → 2s → 4s → 8s ... 最大 30s
二进制协议对于高频数据(如游戏状态同步),使用 Protobuf/MessagePack 替代 JSON,减少序列化开销
水平扩展通过 Redis Pub/Sub 或 Kafka 在 WebSocket 服务集群间广播消息

10. 在线演示 — 帧的构造与解析

帧构造器 (Frame Builder)

输入消息,模拟浏览器构造 WebSocket 帧的过程。可以看到完整的帧字节结构。

点击"构造帧"查看帧的二进制结构...
点击"解析"模拟服务端解码过程...

协议模拟 — 握手 + 消息收发

模拟一次完整的 WebSocket 通信过程。

点击按钮开始模拟...

WebSocket Protocol — RFC 6455 | 技术深入学习 · 第一性原理