AI 热点监控工具用 WebSocket 长连接替代客户端定时轮询,把”客户端反复拉取”变成”服务端有新热点即刻推送”。WebSocket 经一次 HTTP 握手升级为全双工通道,单连接双向收发,配合应用层心跳与指数退避重连,做到断线秒级恢复、消息不丢。轮询在更新频繁时浪费大量空请求,延迟还受轮询间隔限制,因此被这套系统弃用。
一、为什么不用轮询
短轮询让客户端按固定周期发请求,无论有无新数据都消耗一次 HTTP 往返;热点密集时带宽被空响应吃光,且端到端延迟基本等于轮询周期。长轮询让服务端把请求挂起直到有数据才返回,虽减少了空请求,但每次响应都重建连接,并发高时服务端句柄与上下文切换压力陡增。
WebSocket 握手后保持一条 TCP 长连接,服务端探测到新热点直接以帧推送,延迟更低、单消息开销更小,还能同时接收客户端发来的订阅偏好与控制信令。三者差异如下:
| 传输方式 | 数据方向 | 连接模型 | 端到端延迟 | 自动重连 | 典型适用 |
|---|---|---|---|---|---|
| 短轮询 | 客户端 → 服务端 | 每次新请求 | 高(≈周期) | 手动 | 低频更新 |
| 长轮询 | 客户端 → 服务端 | 阻塞后重建 | 中 | 手动 | 受限网络环境 |
| WebSocket | 双向 | 持久连接 | 极低 | 需手动实现 | 高频双向推送 |
| SSE | 服务端 → 客户端 | 持久连接 | 极低 | 浏览器内置 | 单向推送 |
二、握手与帧结构
客户端先发普通 HTTP 请求,带 Upgrade: websocket 头,服务端回 101 Switching Protocols 完成协议切换。切换后连接不再是请求-响应模式,双方交换二进制帧。常用帧类型:
0x1文本帧,承载 UTF-8 编码的 JSON 消息;0x2二进制帧,承载 Protobuf、MessagePack 等紧凑数据;0x8关闭帧,协商优雅断开;0x9Ping 帧,0xAPong 帧,用于保活探测。
帧头仅 2–10 字节,消息开销远低于每次 HTTP 携带的请求头,这也是长连接省流量的根源。
三、连接保活:心跳与重连
真实网络不可靠,用户切 Wi-Fi、休眠、掉 VPN 时,服务端未必立刻感知,于是出现”半开连接”——链路已断,服务端却还持有资源。应用层心跳解决该问题:每 15–30 秒发一次 ping,收到 pong 即标记存活,超时未响应则 terminate 清理。
重连同样关键。若服务端崩溃、十万客户端同时重连,会酿成二次雪崩,因此用指数退避叠加随机抖动打散洪峰。断连期间产生的热点,则靠自增序列号做断点续传补齐。
- 客户端
onclose触发后计算退避延迟,初始 1 秒,每次翻倍,封顶 30 秒; - 叠加 0–1 秒随机抖动,避免所有客户端在同一时刻重连;
- 重连成功后发送
subscribe订阅偏好,并把退避复位为初始值; - 带上最后收到的序列号向服务端请求缺失消息,补齐断连窗口内的热点。
服务端心跳实现(Node.js,ws 库):
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.isAlive = true;
ws.on('pong', () => { ws.isAlive = true; });
ws.on('message', (buf) => {
const msg = JSON.parse(buf.toString());
if (msg.type === 'subscribe') ws.topics = msg.topics;
});
});
const timer = setInterval(() => {
wss.clients.forEach((ws) => {
if (ws.isAlive === false) return ws.terminate();
ws.isAlive = false;
ws.ping();
});
}, 30000);
客户端重连实现:
let ws;
let retry = 1000;
const MAX = 30000;
function connect() {
ws = new WebSocket('wss://example.com/ws');
ws.onopen = () => {
retry = 1000;
ws.send(JSON.stringify({ type: 'subscribe', topics: ['tech', 'finance'] }));
};
ws.onmessage = (e) => renderHotspot(JSON.parse(e.data));
ws.onclose = () => {
const jitter = Math.random() * 1000;
setTimeout(connect, Math.min(retry, MAX) + jitter);
retry = Math.min(retry * 2, MAX);
};
}
connect();
四、横向扩展与消息投递
单机连接数有上限,多节点部署时需把广播解耦到共享总线。常见做法是 Redis Pub/Sub:每个网关节点订阅同一频道,任一节点收到新热点就 PUBLISH,所有节点向本地连接推送。负载均衡器需开启 sticky session,或把连接归属存到共享存储,否则重连会被路由到无状态的节点。
消息可靠性靠自增序列号保证。每条热点带全局递增序号,客户端重连时上报”最后收到序号 N”,服务端补发 N 之后的全部消息。WebSocket 只提供通道,不保证持久投递,需自行补齐。
4.1 用 Redis 做跨节点广播
每个网关节点订阅同一频道,任一节点收到新热点就 PUBLISH,所有节点向本地连接推送。负载均衡器需开启 sticky session,或把连接归属存到共享存储,否则重连会被路由到无状态的节点。
常见问题(FAQ)
Q1:热点监控用 SSE 不行吗?
单向推送 SSE 更省事且浏览器自带重连;本项目需回传订阅偏好,故用 WebSocket。
Q2:心跳间隔设多少合适?
15–30 秒发一次应用层 ping 即可,过频耗电耗流量,过疏难以及时清理死连接。
Q3:重连后消息会丢吗?
靠自增序列号做断点续传,重连时带上最后序号,服务端补发窗口内遗漏的热点。