跳至正文

WebSocket

WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。客户端和服务端可以随时互相推送数据,适合需要双向低延迟通信的场景。

Note

特点

  • 全双工通信:客户端和服务端可以同时收发,适合聊天、协同编辑、在线游戏
  • 持久连接:一次握手后保持连接,避免 HTTP 反复建连的开销
  • 低延迟:无需轮询,数据变化时即时推送
  • 二进制支持:可传输文本和二进制(ArrayBuffer / Blob

Note

局限

  • 有状态连接:服务端需维护每个连接的内存,难以水平扩展,多服务器需引入消息中间件(Redis Pub/Sub 等)
  • 无法利用 HTTP 缓存Cache-ControlETag 等)
  • 浏览器无法发送协议层 Ping:前端心跳需用应用层消息模拟
  • 无同源策略:任何网页都能连接,必须在服务端验证 Origin
  • 过度设计风险:不需要双向通信的场景用 WebSocket 反而维护成本高,大多数”实时”场景 SSE 就够

Note

适用场景

聊天室、在线游戏、协同编辑、实时交易——需要双方随时互发消息的场景。单向推送用 SSE 即可,选型见 API 通信总览

HTTP 轮询 vs WebSocket

┏━━━━━━━ HTTP 轮询(Polling)━━━━━━━━┓
┃                                     ┃
┃  Client ━━ GET /data ━━▶ Server     ┃  → 有数据就返回,没数据返回空
┃  Client ━━ GET /data ━━▶ Server     ┃  → 每隔 N 秒重复请求
┃  Client ━━ GET /data ━━▶ Server     ┃  → 大量无效请求,浪费带宽
┃                                     ┃
┃  单向:只能客户端主动请求           ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛


┏━━━━━━━━ WebSocket ━━━━━━━━━━━━━━━━━┓
┃                                     ┃
┃  Client ━━ HTTP Upgrade ━━▶ Server  ┃  → 握手(仅一次)
┃  Client ◀━━━━ data ━━━━━▶ Server    ┃  → 双向实时通信
┃  Client ◀━━━━ data ━━━━━▶ Server    ┃  → 无需重复建连
┃                                     ┃
┃  双向:服务端也可以主动推送         ┃
┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛

基础概念

概念一句话说明
握手(Handshake)客户端发送 HTTP Upgrade 请求,服务端返回 101 切换协议
帧(Frame)WebSocket 传输数据的最小单位,包含 opcode、payload 等
心跳(Ping/Pong)定期发送控制帧检测连接是否存活
关闭(Close)任一方发送关闭帧,双方完成四次挥手断开连接
子协议(Subprotocol)在 WebSocket 之上约定的应用层协议(如 STOMP、GraphQL over WS)

握手过程

Note

要点:WebSocket 握手基于 HTTP,但握手完成后就不再是 HTTP 了

  • 握手阶段是标准 HTTP 请求,因此能通过 HTTP 代理和负载均衡
  • 握手成功后协议升级为 WebSocket,后续通信走 WebSocket 帧,不再是 HTTP 报文
  • 这就是为什么 WebSocket 的 URL 是 ws://wss://

握手请求(客户端 → 服务端)

GET /chat HTTP/1.1
Host: example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Sec-WebSocket-Protocol: chat, superchat
请求头说明
Upgrade: websocket请求将协议升级为 WebSocket
Connection: Upgrade表示这是一个升级连接的请求
Sec-WebSocket-Key随机生成的 Base64 编码字符串,用于验证服务端身份
Sec-WebSocket-VersionWebSocket 协议版本(固定 13
Sec-WebSocket-Protocol可选,客户端支持的子协议列表

握手响应(服务端 → 客户端)

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
响应头说明
101 Switching Protocols协议切换成功
Sec-WebSocket-AcceptSec-WebSocket-Key + 固定 GUID 经 SHA-1 + Base64 算出,防止伪造

Note

Sec-WebSocket-Accept 的计算

Sec-WebSocket-Accept = Base64(SHA1(Sec-WebSocket-Key + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"))

这不是加密,而是证明服务端确实理解 WebSocket 协议,不是一个普通的 HTTP 服务器意外响应了 101。

连接生命周期

Client                                    Server
  │                                          │
  │─── HTTP GET (Upgrade: websocket) ──────▶│  ① 握手请求
  │◀── HTTP 101 Switching Protocols ────────│  ② 握手响应
  │                                          │
  │◀═══════ 双向数据传输 ═══════════════════▶│  ③ 通信阶段
  │─── Ping ────────────────────────────────▶│  ④ 心跳检测
  │◀── Pong ─────────────────────────────────│
  │                                          │
  │─── Close Frame ─────────────────────────▶│  ⑤ 关闭连接
  │◀── Close Frame ──────────────────────────│

数据帧格式

WebSocket 通过帧(Frame) 传输数据,每个帧的结构如下:

 0                   1                   2                   3
 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-------+-+-------------+-------------------------------+
|F|R|R|R| opcode|M| Payload len |    Extended payload length   |
|I|S|S|S|  (4)  |A|     (7)     |           (16/64)            |
|N|V|V|V|       |S|             |   (if payload len==126/127)  |
| |1|2|3|       |K|             |                              |
+-+-+-+-+-------+-+-------------+- - - - - - - - - - - - - - - +
|                               |                              |
|  Masking-key (0 or 4 bytes)   |                              |
+-------------------------------+------------------------------+
|                     Payload Data                             |
+--------------------------------------------------------------+
字段位数说明
FIN1是否为消息的最后一帧(1 = 最后一帧)
RSV1/2/3各 1预留位,通常为 0(扩展协议使用)
opcode4帧类型:0x1 文本、0x2 二进制、0x8 关闭、0x9 Ping、0xA Pong
MASK1客户端发送的帧必须掩码(MASK=1),服务端发送不掩码
Payload len7 / 7+16 / 7+64数据长度(0–125 / 126 用 16bit 扩展 / 127 用 64bit 扩展)
Masking-key0 或 32MASK=1 时存在,4 字节随机值
Payload Data变长业务数据,客户端发送时按 Masking-key 逐字节异或

Note

要点

  • 客户端 → 服务端的数据必须掩码,服务端 → 客户端不掩码(协议强制规定)
  • 大消息会被分为多个帧传输(分片),接收端根据 FIN 位判断消息是否完整
  • Ping/Pong 是控制帧,用于心跳检测,不携带业务数据

心跳机制(Ping/Pong)

WebSocket 连接可能因网络异常而”半死”(TCP 连接还在但实际不可达),心跳用于发现并清理这类死连接。

正常情况:
Client ─── Ping ──▶ Server
Client ◀── Pong ─── Server     ← 连接存活

异常情况:
Client ─── Ping ──▶ Server
(等待超时,无 Pong 响应)       ← 连接已死,主动断开

Note

要点

  • Ping/Pong 是 WebSocket 协议层的控制帧,不是应用层自己发 { type: "ping" } 消息
  • 收到 Ping 必须回复 Pong(浏览器自动处理,Node.js 的 ws 库需手动或依赖库内置行为)
  • 心跳间隔一般 30~60 秒,过短浪费带宽,过长检测不及时
  • 浏览器限制:浏览器的 WebSocket API 无法发送协议层 Ping 帧,前端通常用应用层消息(如 { type: "ping" })模拟,服务端需配合回复

服务端实现见 Node.js 实现

浏览器端 API

const ws = new WebSocket("ws://localhost:3000");

ws.onopen = () => {
  ws.send("Hello"); // 发送文本
  ws.send(new Blob(["binary data"])); // 发送二进制
  ws.send(JSON.stringify({ type: "join", room: 1 })); // 发送 JSON
};

ws.onmessage = (event) => {
  console.log("收到:", event.data); // string 或 Blob
};

ws.onclose = (event) => {
  console.log(`关闭: code=${event.code}, reason=${event.reason}`);
  // event.wasClean 表示是否正常关闭
};

ws.onerror = (error) => {
  console.error("错误:", error);
};

// 主动关闭
ws.close(1000, "Normal closure");

Note

readyState 四种状态

常量含义
0WebSocket.CONNECTING连接中,尚未建立
1WebSocket.OPEN连接已建立,可通信
2WebSocket.CLOSING关闭中,已发送关闭帧
3WebSocket.CLOSED已关闭或连接失败

发送数据前必须检查 readyState === 1,否则会抛异常。

安全性

wss:// 加密

ws:// 是明文传输,生产环境必须使用 wss://(WebSocket over TLS),等同于 HTTPS 对 HTTP 的关系。

ws://example.com/chat     → 明文,不安全
wss://example.com/chat    → TLS 加密,生产必须

常见安全问题

问题说明防御
跨站 WebSocket 劫持(CSWSH)浏览器发起 WebSocket 握手时自动带上同域 Cookie(不受 SameSite=Lax 以外的限制),恶意页面可借此发起已认证连接服务端握手阶段验证 Origin 头 + SameSite=Strict Cookie
认证WebSocket 握手是 HTTP,之后就不是了,握手后无法再走 Cookie/Token 流程握手阶段验证(URL 参数、Cookie),或连接后首条消息发送 Token
DoS恶意客户端发送大量连接或超大消息限制并发连接数、消息大小、速率

Note

要点:WebSocket 没有同源策略

HTTP 有 CORS 限制,但 WebSocket 没有同源策略,任何网页都可以向任意 WebSocket 服务发起连接。因此必须在服务端验证 Origin

关闭状态码

状态码含义常见场景
1000正常关闭主动调用 ws.close()
1001端点离开页面跳转、服务端关闭
1002协议错误收到不符合协议的帧
1003数据类型错误收到不支持的数据类型
1006异常关闭没有收到关闭帧(网络断开)
1008违反策略消息违反服务端策略
1009消息过大超出服务端最大消息限制
1011意外错误服务端遇到意外情况

1006 不能通过代码手动发送,只在连接异常断开时由浏览器自动设置。

子协议与常见应用

WebSocket 只定义了”帧 + 双向传输”,具体消息格式和交互语义通常由子协议(Subprotocol)约定,握手时通过 Sec-WebSocket-Protocol 协商。常见子协议:

子协议用途
graphql-transport-wsgraphql-ws 库)现代 GraphQL Subscription 的事实标准
graphql-ws(旧的 subscriptions-transport-wsApollo 2.x 时代的 GraphQL Subscription,已废弃
mqttIoT 消息协议走 WebSocket(mqtt.js
stomp面向消息的简单文本协议(RabbitMQ、ActiveMQ 浏览器客户端)
自定义 JSON / 二进制大多数业务自定义协议走这条

Note

要点

子协议让同一个 WebSocket 端点能同时支持多种消息约定。客户端和服务端必须握手时协商一致,否则连接建立后会因消息格式不匹配而混乱。

消息压缩:permessage-deflate

RFC 7692 定义的 WebSocket 压缩扩展。ws 库、Socket.IO 默认启用;浏览器也支持。

  • 大文本消息(JSON)压缩率可达 60–80%
  • 小消息(< 几百字节)压缩反而更慢、更大
  • CPU 敏感或高频小消息场景建议关闭:new WebSocketServer({ perMessageDeflate: false })

扩展性与生态

  • 服务端实现要点(ws 库 / Socket.IO、心跳、房间)见 Node.js 实现
  • 多实例部署的水平扩展方案见 水平扩展