实时功能已成为现代 Web 应用程序的基本期望。 WebSocket 通过单个 TCP 连接提供全双工通信通道,从而实现客户端和服务器之间的低延迟数据交换。本指南涵盖了 WebSocket 协议、实现模式、扩展策略以及构建生产级实时应用程序的实际注意事项。
WebSocket 协议概述
WebSocket 以 HTTP 升级握手开始。客户端发送带有“Upgrade: websocket”标头的 HTTP 请求,服务器响应“101 Switching Protocols”以建立连接。一旦建立,连接就会在同一底层 TCP 套接字上从 HTTP 转换到 WebSocket 协议。
Client → Server: GET /ws HTTP/1.1
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Server → Client: HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
帧是WebSocket通信的基本单位。文本帧携带 UTF-8 编码消息,二进制帧携带原始二进制数据,例如 ArrayBuffer 或 Blob,控制帧通过 Ping、Pong 和 Close 消息管理连接生命周期。
连接生命周期
管理 WebSocket 连接需要处理六种不同的状态:通过服务器端验证建立连接、双向通信的打开状态、入站和出站数据的消息处理、通过定期乒乓球检测陈旧连接的心跳、使用适当的关闭代码正常关闭以及网络故障和协议违规的错误处理。
const ws = new WebSocket("wss://api.example.com/ws");
ws.onopen = () => console.log("Connected");
ws.onmessage = (event) => handleMessage(event.data);
ws.onclose = (event) => handleDisconnect(event.code, event.reason);
ws.onerror = (error) => console.error("WebSocket error:", error);
关闭代码传达终止原因:1000 表示正常关闭,1001 表示端点即将消失,1002 表示协议错误,1011 表示意外的服务器情况。选择正确的关闭代码有助于客户端正确处理重新连接。
重新连接策略
在任何实时应用程序中,网络中断都是不可避免的。强大的重新连接策略使用带有抖动的指数退避来防止许多客户端同时重新连接时出现雷群问题。
class ReconnectingWebSocket {
constructor(url, options = {}) {
this.url = url;
this.maxRetries = options.maxRetries || Infinity;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 30000;
this.retryCount = 0;
this.connect();
}
connect() {
this.ws = new WebSocket(this.url);
this.ws.onclose = () => this.scheduleReconnect();
}
scheduleReconnect() {
const delay = Math.min(
this.baseDelay * Math.pow(2, this.retryCount),
this.maxDelay
) + Math.random() * 1000;
setTimeout(() => this.connect(), delay);
this.retryCount++;
}
}
除了退避之外,最佳实践还包括设置最大重试限制、跟踪连接状态(已连接、重新连接、断开连接)、缓冲重新连接期间发送的消息以及在建立持续连接后重置重试计数。
二进制消息和压缩
对于高性能应用程序,与 JSON 文本帧相比,二进制消息可以减少开销。使用 ArrayBuffer 和 DataView 可以高效地编码和解码结构化数据。
const buffer = new ArrayBuffer(4);
const view = new DataView(buffer);
view.setUint32(0, 12345);
ws.send(buffer);
ws.binaryType = "arraybuffer";
ws.onmessage = (event) => {
const view = new DataView(event.data);
const value = view.getUint32(0);
};
每消息压缩 (PMCE) 压缩进一步将文本消息的带宽减少 60-80%。但是,它会增加 CPU 开销,并且可能会增加小负载的延迟。压缩对于已经压缩的数据(例如图像或视频)没有好处。根据您的消息模式有选择地启用它。
使用 Redis Pub/Sub 进行扩展
单服务器 WebSocket 部署不能水平扩展。 Redis pub/sub 通过启用跨多个应用程序服务器的消息广播来解决这个问题。
Client A → Server 1 → Redis (publish message)
↓
Client B ← Server 2 ← Redis (subscribe, broadcast)
每个服务器都订阅 Redis 频道。当服务器接收到来自客户端的消息时,它会发布到 Redis。所有其他服务器通过其订阅接收消息并将其广播到其连接的客户端。该模式支持基于房间的广播以进行选择性消息传递,并且可以使用 Socket.IO 或“ws”等库与“ioredis”相结合来实现。
与 SSE 和长轮询的比较
| 特色 | WebSocket | 上交所 | 长轮询 |
|---|---|---|---|
| 方向 | 双向 | 服务器→客户端 | 双向(民意调查) |
| 协议 | ws:// | HTTP | HTTP |
| 延迟 | 低 | 低 | 高 |
| 浏览器支持 | 通用 | 好(没有IE) | 通用 |
| 重新连接 | 手册 | 内置 | 手册 |
| 二进制支持 | 是的 | 否(仅文本) | 是的 |
当您仅需要服务器到客户端流来处理股票行情或通知等用例时,请使用服务器发送的事件。当聊天、协作编辑或实时游戏需要双向通信时,请使用 WebSocket。
安全考虑
在生产中,始终仅使用“wss://”(基于 TLS 的 WebSocket)。验证源标头以防止跨站点 WebSocket 劫持、在连接 URL 或初始握手消息中实施身份验证令牌、清理服务器端的所有消息内容、设置连接超时和消息大小限制,以及对每个 IP 地址的新连接尝试进行速率限制。子协议协商提供了一种机制,用于随着 WebSocket API 的发展对其进行版本控制。
## 结论
WebSocket 仍然是实时 Web 通信的黄金标准。生产中的成功需要关注完整的连接生命周期、具有指数退避的强大重新连接逻辑、通过 Redis 发布-订阅的智能扩展以及谨慎的安全实践。虽然 SSE 和长轮询各有千秋,但 WebSocket 的双向、低延迟特性使其成为要求实时应用程序的正确选择。

