WebSocket
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。客户端和服务端可以随时互相推送数据,适合需要双向低延迟通信的场景。
Note
特点
- 全双工通信:客户端和服务端可以同时收发,适合聊天、协同编辑、在线游戏
- 持久连接:一次握手后保持连接,避免 HTTP 反复建连的开销
- 低延迟:无需轮询,数据变化时即时推送
- 二进制支持:可传输文本和二进制(
ArrayBuffer/Blob)
Note
局限
- 有状态连接:服务端需维护每个连接的内存,难以水平扩展,多服务器需引入消息中间件(Redis Pub/Sub 等)
- 无法利用 HTTP 缓存(
Cache-Control、ETag等) - 浏览器无法发送协议层 Ping:前端心跳需用应用层消息模拟
- 无同源策略:任何网页都能连接,必须在服务端验证
Origin - 过度设计风险:不需要双向通信的场景用 WebSocket 反而维护成本高,大多数”实时”场景 SSE 就够
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-Version | WebSocket 协议版本(固定 13) |
Sec-WebSocket-Protocol | 可选,客户端支持的子协议列表 |
握手响应(服务端 → 客户端)
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
| 响应头 | 说明 |
|---|---|
101 Switching Protocols | 协议切换成功 |
Sec-WebSocket-Accept | 用 Sec-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 |
+--------------------------------------------------------------+
| 字段 | 位数 | 说明 |
|---|---|---|
FIN | 1 | 是否为消息的最后一帧(1 = 最后一帧) |
RSV1/2/3 | 各 1 | 预留位,通常为 0(扩展协议使用) |
opcode | 4 | 帧类型:0x1 文本、0x2 二进制、0x8 关闭、0x9 Ping、0xA Pong |
MASK | 1 | 客户端发送的帧必须掩码(MASK=1),服务端发送不掩码 |
Payload len | 7 / 7+16 / 7+64 | 数据长度(0–125 / 126 用 16bit 扩展 / 127 用 64bit 扩展) |
Masking-key | 0 或 32 | MASK=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 秒,过短浪费带宽,过长检测不及时
- 浏览器限制:浏览器的
WebSocketAPI 无法发送协议层 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 四种状态
| 值 | 常量 | 含义 |
|---|---|---|
0 | WebSocket.CONNECTING | 连接中,尚未建立 |
1 | WebSocket.OPEN | 连接已建立,可通信 |
2 | WebSocket.CLOSING | 关闭中,已发送关闭帧 |
3 | WebSocket.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-ws(graphql-ws 库) | 现代 GraphQL Subscription 的事实标准 |
graphql-ws(旧的 subscriptions-transport-ws) | Apollo 2.x 时代的 GraphQL Subscription,已废弃 |
mqtt | IoT 消息协议走 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 实现
- 多实例部署的水平扩展方案见 水平扩展