WebSocket 实时推送实现方法详解(热点监控为何弃用轮询)

AI 热点监控工具用 WebSocket 长连接替代客户端定时轮询,把”客户端反复拉取”变成”服务端有新热点即刻推送”。WebSocket 经一次 HTTP 握手升级为全双工通道,单连接双向收发,配合应用层心跳与指数退避重连,做到断线秒级恢复、消息不丢。轮询在更新频繁时浪费大量空请求,延迟还受轮询间隔限制,因此被这套系统弃用。

一、为什么不用轮询

短轮询让客户端按固定周期发请求,无论有无新数据都消耗一次 HTTP 往返;热点密集时带宽被空响应吃光,且端到端延迟基本等于轮询周期。长轮询让服务端把请求挂起直到有数据才返回,虽减少了空请求,但每次响应都重建连接,并发高时服务端句柄与上下文切换压力陡增。

WebSocket 握手后保持一条 TCP 长连接,服务端探测到新热点直接以帧推送,延迟更低、单消息开销更小,还能同时接收客户端发来的订阅偏好与控制信令。三者差异如下:

传输方式 数据方向 连接模型 端到端延迟 自动重连 典型适用
短轮询 客户端 → 服务端 每次新请求 高(≈周期) 手动 低频更新
长轮询 客户端 → 服务端 阻塞后重建 中 手动 受限网络环境
WebSocket 双向 持久连接 极低 需手动实现 高频双向推送
SSE 服务端 → 客户端 持久连接 极低 浏览器内置 单向推送

二、握手与帧结构

客户端先发普通 HTTP 请求,带 Upgrade: websocket 头,服务端回 101 Switching Protocols 完成协议切换。切换后连接不再是请求-响应模式,双方交换二进制帧。常用帧类型:

  • 0x1 文本帧,承载 UTF-8 编码的 JSON 消息;
  • 0x2 二进制帧,承载 Protobuf、MessagePack 等紧凑数据;
  • 0x8 关闭帧,协商优雅断开;
  • 0x9 Ping 帧,0xA Pong 帧,用于保活探测。

帧头仅 2–10 字节,消息开销远低于每次 HTTP 携带的请求头,这也是长连接省流量的根源。

三、连接保活:心跳与重连

真实网络不可靠,用户切 Wi-Fi、休眠、掉 VPN 时,服务端未必立刻感知,于是出现”半开连接”——链路已断,服务端却还持有资源。应用层心跳解决该问题:每 15–30 秒发一次 ping,收到 pong 即标记存活,超时未响应则 terminate 清理。

重连同样关键。若服务端崩溃、十万客户端同时重连,会酿成二次雪崩,因此用指数退避叠加随机抖动打散洪峰。断连期间产生的热点,则靠自增序列号做断点续传补齐。

  1. 客户端 onclose 触发后计算退避延迟,初始 1 秒,每次翻倍,封顶 30 秒;
  2. 叠加 0–1 秒随机抖动,避免所有客户端在同一时刻重连;
  3. 重连成功后发送 subscribe 订阅偏好,并把退避复位为初始值;
  4. 带上最后收到的序列号向服务端请求缺失消息,补齐断连窗口内的热点。

服务端心跳实现(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:重连后消息会丢吗?

靠自增序列号做断点续传,重连时带上最后序号,服务端补发窗口内遗漏的热点。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部