这是《计算机网络 零基础实战指南》的独立章节版。本章从概念、实操和生产排查三个视角展开,代码块保留了原书可直接运行的版本。 HTTP 是请求响应模型,服务端主动推送并不自然。WebSocket 通过一次 HTTP Upgrade 建立全双工长连接,适合聊天、协同编辑、实时行情、游戏和推送。长连接的价值是降低建连成本,挑战是连接治理、心跳、断线重连、广播扇出和故障切换。
18.1 从 HTTP 到 WebSocket
握手请求:
GET /ws HTTP/1.1
Host: ws.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Sec-WebSocket-Version: 13
Origin: https://www.example.com
握手响应:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
握手完成后,连接不再使用 HTTP 请求响应语义,改用 WebSocket 帧。
18.2 帧格式
WebSocket 消息由帧组成:
| Opcode | 含义 |
|---|---|
| 0x1 | 文本帧 |
| 0x2 | 二进制帧 |
| 0x8 | 连接关闭 |
| 0x9 | Ping |
| 0xA | Pong |
| 0x0 | continuation |
特性:
- 支持文本和二进制;
- 支持消息分片;
- 支持控制帧;
- 客户端到服务端的帧必须掩码;
- 有消息边界;
- 默认使用 TCP 字节流承载。
18.3 使用 wscat
npm install -g wscat
wscat -c ws://127.0.0.1:8080/ws
wscat -c wss://ws.example.com/ws
发送消息后可观察服务端日志和帧解析。调试时注意 URL 协议是 ws 或 wss,不是 http。
18.4 心跳与假死
长连接可能被以下设备悄悄断开:
- 负载均衡空闲超时;
- NAT 超时;
- 防火墙会话超时;
- 代理连接池回收;
- 服务端主动关闭。
策略:
客户端每 25 秒发送 Ping
服务端收到后回 Pong
服务端读空闲 60 秒触发关闭
客户端连续 2 次未收到 Pong 则重连
心跳间隔必须小于链路中最短空闲超时。
18.5 断线重连
客户端重连要求:
- 指数退避加随机抖动;
- 最大重连间隔;
- 会话恢复令牌;
- 消息序号或版本号;
- 补拉离线消息;
- 去重;
- 网络切换后快速重试;
- 服务端可下发重试建议。
错误示例:
断线后每 100ms 重连
-> 服务端尚未恢复
-> 连接风暴
-> 雪崩
正确示例:
1s、2s、4s、8s,最大 60s,加 0-500ms 随机抖动
18.6 连接治理
服务端要治理:
| 项目 | 建议 |
|---|---|
| 单机连接数 | 设置上限和监控 |
| 单 IP 连接数 | 限流,防滥用 |
| 用户连接数 | 同一用户多端策略 |
| 消息大小 | 限制帧和消息上限 |
| 发送队列 | 控制慢消费者 |
| 空闲时间 | 心跳和读超时 |
| 广播扇出 | 分组和分片 |
| 优雅关闭 | 先停止接收,再排空 |
慢消费者问题:
客户端接收慢
-> 服务端为它积压消息
-> 内存上涨
-> 影响其他连接
处理:
- 每连接发送队列上限;
- 超限断开;
- 关键消息和可丢消息分级;
- 广播降级为最新值;
- 使用有界队列;
- 监控队列长度。
18.7 Spring Boot 示例
依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-websocket</artifactId>
</dependency>
配置:
@Configuration
@EnableWebSocket
public class WebSocketConfig implements WebSocketConfigurer {
@Override
public void registerWebSocketHandlers(
WebSocketHandlerRegistry registry) {
registry.addHandler(new EchoHandler(), "/ws")
.setAllowedOrigins("https://www.example.com");
}
}
处理器:
public class EchoHandler extends TextWebSocketHandler {
@Override
protected void handleTextMessage(
WebSocketSession session,
TextMessage message) throws Exception {
session.sendMessage(
new TextMessage("ack:" + message.getPayload()));
}
}
生产实现还需心跳、异常处理、连接统计和限流。
18.8 集群与广播
多节点长连接服务需要跨节点推送:
用户 A 连接 Node 1
用户 B 连接 Node 2
系统事件发布到 Kafka / Redis / MQ
-> 所有节点收到
-> 各节点推给本机连接
要点:
- 连接路由表;
- 用户到节点映射;
- 消息可靠性和顺序;
- 重复消息去重;
- 节点故障切换;
- 广播风暴控制;
- 离线消息存储;
- 监控端到端延迟。
18.9 代理与负载均衡
反向代理必须支持 Upgrade:
location /ws {
proxy_pass http://websocket-backend;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_read_timeout 300s;
proxy_send_timeout 300s;
}
注意:
proxy_read_timeout应大于心跳间隔;- LB 空闲超时要同步调整;
- TLS 使用
wss://; - 保持客户端真实 IP;
- 健康检查不能只看端口;
- 会话粘滞视业务状态而定。
18.10 安全
- 强制
wss://; - 校验 Origin;
- 握手时完成认证;
- 短期 token 或 Ticket;
- 限制消息大小;
- 防止注入和 XSS;
- 每次业务消息授权;
- 不信任客户端上报的设备信息;
- 监控异常连接和消息频率。
本章小结
WebSocket 通过 HTTP Upgrade 建立全双工长连接,使用帧协议承载文本和二进制消息。长连接治理重点是心跳、断线重连、连接上限、慢消费者和集群广播。反向代理和 LB 必须支持 Upgrade 并保证超时大于心跳间隔,安全上应使用 wss、校验 Origin 并在握手和业务消息两层授权。
思考题
- WebSocket 握手为什么返回 101?
- 心跳间隔和 LB 空闲超时应满足什么关系?
- 如何避免断线重连风暴?
- 慢消费者为什么可能拖垮服务?
- 设计一个百万连接推送系统的架构。