WebSocket 深度解析
打破 HTTP 的枷锁,开启实时双向通信的新纪元
RFC 6455 · 2024.121. 什么是 WebSocket?
WebSocket 是一种在单个 TCP 连接上进行全双工通信的协议。它允许服务端主动向客户端推送数据,彻底改变了 HTTP “请求-响应”的单向通信模式。
核心特点:
- 建立在 TCP 之上:基于可靠的 TCP 协议,保证数据传输的稳定性和完整性。
- 复用 HTTP 握手:通过 HTTP
Upgrade机制建立连接,兼容现有网络基础设施。 - 全双工:数据可以同时双向流动,服务端和客户端地位平等。
- 轻量级:数据帧头部最小仅 2 字节,相比 HTTP 几百字节头部大大减少开销。
- 持久连接:一次握手,长久使用,避免频繁建立连接的开销。
协议标识:ws:// 表示普通 WebSocket 连接(端口 80),wss:// 表示加密连接(端口 443),类似于 HTTP 和 HTTPS 的关系。
2. 为什么需要 WebSocket?
在 WebSocket 出现之前,实现“实时通信”必须依赖以下两种变通方案:
短轮询 (Short Polling)
客户端每隔固定时间(如 3 秒)发起 HTTP 请求查询新数据。
- 大量空请求浪费带宽
- 实时性差(有轮询间隔延迟)
- 服务器压力大
长轮询 (Long Polling)
客户端发起请求,服务器挂起直到有新数据才返回。
- 服务器维护大量挂起连接
- 每次重连都要重发 HTTP 头
- 仍是单向通信模式
WebSocket 解决方案
一次握手,建立持久连接,数据双向实时流动,彻底解决上述痛点。
3. WebSocket 工作原理
WebSocket 的生命周期分为两个阶段:握手升级 和 数据传输。
握手过程 (Handshake)
客户端发送带有 Upgrade: websocket 头的 HTTP 请求,服务器若支持则返回 101 Switching Protocols,协议即从 HTTP 升级为 WebSocket。
关键请求头字段:
| 字段 | 说明 |
|---|---|
Upgrade: websocket |
声明协议升级类型 |
Connection: Upgrade |
表示需要升级连接 |
Sec-WebSocket-Key |
Base64 编码的随机值,用于安全验证 |
Sec-WebSocket-Version |
协议版本,当前为 13 |
readyState 连接状态
WebSocket 对象的 readyState 属性表示当前连接状态:
| 值 | 常量 | 说明 |
|---|---|---|
0 |
CONNECTING | 正在建立连接 |
1 |
OPEN | 连接已建立,可正常通信 |
2 |
CLOSING | 连接正在关闭 |
3 |
CLOSED | 连接已关闭 |
数据帧结构 (Data Frame)
连接建立后,双方通过“帧”交换数据。每帧包含极小的头部(最小 2 字节):
| 字段 | 位数 | 说明 |
|---|---|---|
FIN |
1 bit | 1 表示这是消息的最后一帧 |
RSV 1-3 |
3 bits | 保留位,用于扩展 |
Opcode |
4 bits | 操作类型:0x1=文本, 0x2=二进制, 0x8=关闭, 0x9=Ping, 0xA=Pong |
MASK |
1 bit | 客户端发送的数据必须设置为 1 |
Payload Length |
7+ bits | 数据长度(可扩展至 16 或 64 位) |
4. 协议对比分析
HTTP vs WebSocket vs SSE
| 特性 | HTTP | SSE (Server-Sent Events) | WebSocket |
|---|---|---|---|
| 通信方向 | 单向(客户端→服务器) | 单向(服务器→客户端) | 双向(全双工) |
| 连接类型 | 短连接(每次请求新建) | 长连接 | 持久连接 |
| 数据格式 | 文本/二进制 | 仅文本 | 文本/二进制 |
| 协议开销 | 每次携带完整头部 | 仅首次携带头部 | 最小 2 字节帧头 |
| 浏览器支持 | 全部 | 大部分(IE 不支持) | 全部现代浏览器 |
| 自动重连 | N/A | 内置支持 | 需手动实现 |
| 适用场景 | 普通网页请求 | 新闻推送、通知 | 实时聊天、游戏、协作 |
选择建议:如果只需服务器向客户端推送数据(如新闻流、股价更新),SSE 是更简单的选择;如果需要双向实时通信,WebSocket 是最佳方案。
性能对比基准
| 指标 | HTTP 轮询 | WebSocket | 改善幅度 |
|---|---|---|---|
| 单次消息延迟 | 100-500ms | 1-10ms | 50-100x |
| 每条消息开销 | ~800 字节 | 2-14 字节 | 50-400x |
| 服务器并发能力 | ~1万 QPS | ~100万 连接 | 100x |
| CPU 利用率 | 高(频繁连接) | 低(持久连接) | 5-10x |
5. WebSocket 主要优势
低延迟
告别 HTTP 的“请求-响应”往返,服务器有数据直接推送,毫秒级触达。
减少带宽
数据帧头部极小(2-14 字节),HTTP 头部通常几百字节,节省 90%+ 开销。
全双工
类似电话通话,双方可同时发言,无需等待对方说完。
持久连接
一次握手长久使用,避免 TCP 三次握手的重复开销。
二进制支持
原生支持二进制数据传输,适合图片、音频、文件等场景。
跨平台
所有现代浏览器、Node.js、Python 等均有完善支持。
6. 典型应用场景
实时聊天
微信、Slack、Discord 等即时通讯应用的核心技术,消息秒达。
在线游戏
实时同步玩家位置、操作、状态,毫秒必争的竞技场景。
金融行情
股票、加密货币 K 线图实时跳动,行情数据零延迟推送。
协同编辑
Google Docs、Figma 多人同时编辑,实时看到对方光标和改动。
IoT 设备控制
智能家居设备实时状态上报和远程控制指令下发。
监控报警
服务器状态、应用日志实时推送到运维面板。
弹幕系统
B站、直播平台弹幕实时滚动,万人同步。
在线客服
实时聊天、消息已读回执、正在输入提示等功能。
7. 客户端代码示例
基础连接示例
在浏览器中使用 WebSocket 非常简单,原生 API 就足够强大:
// 1. 创建连接
const ws = new WebSocket('wss://echo.websocket.org');
// 2. 连接成功回调
ws.onopen = () => {
console.log('连接已建立!');
ws.send('Hello Server!'); // 发送数据
};
// 3. 接收消息回调
ws.onmessage = (event) => {
console.log('收到消息:', event.data);
};
// 4. 连接关闭回调
ws.onclose = (event) => {
console.log(`连接关闭:码=${event.code} 原因=${event.reason}`);
};
// 5. 错误处理
ws.onerror = (error) => {
console.error('连接错误:', error);
};
完整示例(带重连与心跳)
class WebSocketClient {
constructor(url) {
this.url = url;
this.ws = null;
this.reconnectAttempts = 0;
this.maxReconnectAttempts = 5;
this.heartbeatInterval = null;
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onopen = () => {
console.log('连接成功');
this.reconnectAttempts = 0;
this.startHeartbeat();
};
this.ws.onclose = () => {
this.stopHeartbeat();
this.reconnect();
};
}
startHeartbeat() {
this.heartbeatInterval = setInterval(() => {
if (this.ws.readyState === WebSocket.OPEN) {
this.ws.send(JSON.stringify({ type: 'ping' }));
}
}, 30000); // 每 30 秒发送心跳
}
reconnect() {
if (this.reconnectAttempts < this.maxReconnectAttempts) {
const delay = Math.pow(2, this.reconnectAttempts) * 1000; // 指数退避
setTimeout(() => {
this.reconnectAttempts++;
this.connect();
}, delay);
}
}
}
8. 服务端代码示例
Node.js 示例(使用 ws 库)
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws, req) => {
const ip = req.socket.remoteAddress;
console.log(`客户端连接: ${ip}`);
// 接收消息
ws.on('message', (data) => {
console.log(`收到: ${data}`);
// 广播给所有客户端
wss.clients.forEach(client => {
if (client.readyState === WebSocket.OPEN) {
client.send(data);
}
});
});
// 心跳检测
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
});
// 定时检查连接状态
setInterval(() => {
wss.clients.forEach(ws => {
if (!ws.isAlive) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
Python 示例(使用 websockets 库)
import asyncio
import websockets
async def handler(websocket, path):
async for message in websocket:
print(f"收到: {message}")
await websocket.send(f"回复: {message}")
async def main():
async with websockets.serve(handler, "localhost", 8080):
await asyncio.Future() # 永远运行
asyncio.run(main())
9. 安全性详解
WSS 加密传输
生产环境必须使用 wss:// 协议,它是 WebSocket 的 TLS 加密版本,类似于 HTTPS:
- 数据加密传输,防止窃听
- 防止中间人攻击 (MITM)
- 确保身份验证
Origin 来源验证
服务端应该检查 HTTP 握手时的 Origin 头,防止跨站 WebSocket 劫持 (CSWSH):
// Node.js 服务端验证 Origin
wss.on('connection', (ws, req) => {
const origin = req.headers.origin;
const allowedOrigins = ['https://example.com', 'https://app.example.com'];
if (!allowedOrigins.includes(origin)) {
ws.close(1008, 'Origin not allowed');
return;
}
});
认证机制
WebSocket 不支持自定义头部,常用认证方式:
URL 参数传递 Token
const ws = new WebSocket(
'wss://api.com/ws?token=xxx'
);
首条消息认证
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'auth',
token: 'xxx'
}));
};
注意:通过 URL 传递 Token 可能被记录在服务器日志中,建议使用短期 Token 或首条消息认证方式。
10. 调试技巧
浏览器开发者工具
- Chrome DevTools:打开 Network 面板 → 筛选 WS → 查看 Messages 选项卡
- Firefox:网络面板 → 筛选 WS → 可查看实时消息流
- 在线测试工具:
websocket.org/echo、piesocket.com/websocket-tester
常用关闭码参考
| 状态码 | 含义 |
|---|---|
1000 |
正常关闭 |
1001 |
端点离开(如页面关闭) |
1002 |
协议错误 |
1003 |
不支持的数据类型 |
1006 |
异常关闭(未发送关闭帧) |
1008 |
策略违规(如 Origin 不允许) |
1011 |
服务器内部错误 |
11. 避坑指南
必须使用 WSS
生产环境务必使用 wss:// 加密连接,防止数据被窃听篡改。
心跳保活
网络环境复杂,防火墙可能切断空闲连接。定时发 Ping/Pong 保持活跃。
断线重连
网络波动是常态,必须实现自动重连(推荐指数退避算法)。
消息序列化
统一使用 JSON 或 Protocol Buffers 序列化,定义清晰的消息类型。
负载均衡
WebSocket 是有状态连接,需要粘性会话或使用 Redis Pub/Sub 跨节点通信。
资源清理
及时关闭无效连接,避免内存泄漏和连接数耗尽。
12. 子协议与扩展
WebSocket 子协议
客户端可在握手时通过 Sec-WebSocket-Protocol 头声明支持的子协议:
const ws = new WebSocket('wss://api.com/ws', ['graphql-ws', 'json']);
// 检查服务器选择的子协议
ws.onopen = () => {
console.log('Selected protocol:', ws.protocol);
};
常见子协议
| 子协议 | 用途 |
|---|---|
graphql-ws |
GraphQL 订阅 |
mqtt |
IoT 消息队列 |
stomp |
简单文本消息协议 |
wamp |
RPC 和 Pub/Sub |
扩展 (Extensions)
permessage-deflate 是最常用的扩展,提供消息压缩功能,可减少 60-80% 带宽消耗。
浏览器兼容性
| 浏览器 | 最低版本 | 备注 |
|---|---|---|
| Chrome | 16+ | 完全支持 |
| Firefox | 11+ | 完全支持 |
| Safari | 7+ | 完全支持 |
| Edge | 12+ | 完全支持 |
| IE | 10+ | 基本支持,建议放弃 |
| Node.js | 0.10+ | 使用 ws 库 |
13. 常见问题 (FAQ)
Q: WebSocket 和 Socket.IO 有什么区别?
Socket.IO 是一个库,底层可用 WebSocket,但也支持轮询回退、自动重连、房间等高级功能。纯 WebSocket 更轻量,Socket.IO 功能更丰富。
Q: WebSocket 最大消息大小是多少?
协议理论上支持单条消息最大 2^63 字节。实际中,服务器/客户端通常有限制(如 Node.js ws 库默认 100MB),建议大文件分片传输。
Q: 如何处理高并发 WebSocket 连接?
使用异步框架(Node.js、Go、Rust),单机可轻松撑 10万+ 连接。配合 Redis Pub/Sub 实现跨进程/跨机器消息广播。
Q: WebSocket 能穿透代理和防火墙吗?
使用 wss:// 的 443 端口通常可以。HTTP 代理需要支持 CONNECT 方法,部分企业防火墙可能阻断。
Q: 服务器如何主动关闭连接?
调用 ws.close(code, reason),其中 code 是关闭码(1000-4999),reason 是可选的关闭原因字符串。
Q: WebSocket 如何发送二进制数据?
使用 ws.binaryType = 'arraybuffer' 或 'blob' 设置接收格式,发送时直接传入 ArrayBuffer 或 Blob 对象即可。
Q: 如何实现 WebSocket 服务的水平扩展?
常见方案:1) 使用 Redis Pub/Sub 跨实例广播;2) 使用 Kafka/RabbitMQ 作为消息中心;3) 使用粘性会话 (Sticky Session) 配合负载均衡。
Q: 为什么客户端发送的数据必须掩码?
这是 RFC 6455 的安全要求,防止代理服务器缓存污染攻击。服务端发送的数据不需要掩码,客户端发送必须掩码。