3、WebSocket连接管理:多路复用、心跳保活、断线重连策略
WebSocket,说白了就是交易所和咱们系统之间的一条「专线」。这条线一旦断了,订单流就断了,资金风险就来了。我在做高频交易系统那几年,光是处理断线重连就踩过不少坑。今天咱们就把这块聊透。
3.1 多路复用:一条连接搞定所有
很多人刚开始做量化时,习惯给每个交易对单独开一条WebSocket。比如你交易BTC/USDT、ETH/USDT、SOL/USDT,就开三条连接。这其实是个坏习惯。
为什么?
- 每条连接都有独立的TCP握手开销
- 操作系统对单进程的文件描述符有限制
- 多条连接意味着多个心跳、多个重连逻辑,复杂度翻倍
我建议的做法是:一条连接,多路复用。什么意思?就是只建立一条WebSocket连接,通过订阅机制来区分不同的数据流。
核心思路:连接是物理通道,订阅是逻辑通道。一条物理通道可以承载N个逻辑通道。
来看一个简单的多路复用订阅示例:
// 建立一条连接
const ws = new WebSocket('wss://api.exchange.com/ws');
// 订阅多个交易对
ws.send(JSON.stringify({
type: 'subscribe',
channels: [
{ name: 'orderbook', symbol: 'BTC/USDT' },
{ name: 'orderbook', symbol: 'ETH/USDT' },
{ name: 'trade', symbol: 'SOL/USDT' }
]
}));
// 收到数据时,根据channel字段分发
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
const handler = channelHandlers[data.channel];
if (handler) handler(data);
};
这样做的好处很明显:连接数从N降到了1。我记得有一次在币安做实盘,原本开了12条连接,后来改成多路复用,CPU占用直接降了40%。
小技巧:订阅时尽量用批量接口,别一条一条发。有些交易所的订阅有速率限制,你发太快会被断开。
3.2 心跳保活:别让交易所把你踢了
WebSocket连接有个特点:长时间没数据交互,中间的路由器或防火墙会认为这条连接已经死了,直接给你掐掉。交易所那边也有超时机制,一般60秒没动静就断开。
所以我们需要心跳保活。说白了就是定时发个「我还活着」的信号。
常见的做法有两种:
| 方式 | 原理 | 优缺点 |
|---|---|---|
| Ping/Pong帧 | WebSocket协议自带的控制帧 | 轻量、标准,但部分交易所不支持 |
| 自定义心跳消息 | 发送一个特定的JSON消息 | 兼容性好,但需要服务端配合 |
我个人习惯用自定义心跳。为什么?因为Ping/Pong帧是协议层的,你没法在应用层感知到它是否成功。而自定义心跳可以明确知道「交易所收到了我的心跳,并且回复了」。
// 心跳管理器
class HeartbeatManager {
constructor(ws, interval = 30000) {
this.ws = ws;
this.interval = interval;
this.timer = null;
this.lastPong = Date.now();
}
start() {
this.timer = setInterval(() => {
// 发送心跳
this.ws.send(JSON.stringify({ type: 'ping' }));
// 检查上次回复是否超时
if (Date.now() - this.lastPong > this.interval * 3) {
console.warn('心跳超时,准备重连');
this.ws.close();
}
}, this.interval);
}
onPong() {
this.lastPong = Date.now();
}
stop() {
clearInterval(this.timer);
}
}
注意:心跳间隔别设太短。我曾经设了5秒一次,结果被交易所当成DDoS攻击给封了IP。一般30秒到60秒比较合理。
3.3 断线重连:优雅地处理意外
断线是必然的,不是偶然的。网络抖动、交易所升级、服务器重启……任何环节都可能出问题。关键是断线后怎么处理。
我总结了一套指数退避 + 随机抖动的重连策略:
- 检测到断开:立即尝试重连
- 第一次重连失败:等1秒再试
- 第二次失败:等2秒
- 第三次失败:等4秒
- 以此类推:每次翻倍,直到最大间隔(比如60秒)
- 加上随机抖动:在间隔基础上±20%,防止多个客户端同时重连造成雪崩
来看代码实现:
class ReconnectManager {
constructor(options) {
this.maxRetries = options.maxRetries || 10;
this.baseDelay = options.baseDelay || 1000;
this.maxDelay = options.maxDelay || 60000;
this.retryCount = 0;
this.jitter = 0.2; // 20%抖动
}
getNextDelay() {
// 指数退避
let delay = this.baseDelay * Math.pow(2, this.retryCount);
delay = Math.min(delay, this.maxDelay);
// 随机抖动
const jitterAmount = delay * this.jitter;
delay = delay + (Math.random() * jitterAmount * 2 - jitterAmount);
return Math.floor(delay);
}
async reconnect(connectFn) {
while (this.retryCount < this.maxRetries) {
try {
await connectFn();
console.log(`重连成功,第${this.retryCount + 1}次尝试`);
this.retryCount = 0;
return true;
} catch (err) {
this.retryCount++;
const delay = this.getNextDelay();
console.warn(`重连失败,第${this.retryCount}次,${delay}ms后重试`);
await new Promise(r => setTimeout(r, delay));
}
}
throw new Error('重连次数已达上限');
}
}
关键点:重连成功后,要重新订阅之前的所有频道。我建议把订阅列表持久化到内存中,重连后自动恢复。
3.4 整体架构图
下面这张图展示了WebSocket连接管理的完整流程。你可以看到多路复用、心跳保活、断线重连三者是如何协同工作的。
3.5 避坑指南
最后分享几个我踩过的坑:
- 不要忽略WebSocket的close事件:有些同学只监听error事件,忽略了close。实际上close才是断线的主要信号。
- 重连时清理旧资源:旧的心跳定时器、订阅回调都要清理干净,否则会出现内存泄漏。
- 做好日志记录:每次断线、重连、心跳超时都要打日志。线上排查问题时,这些日志就是你的救命稻草。
- 考虑限流:重连太频繁会被交易所封IP。我建议加上全局的限流器,比如每分钟最多重连5次。
我的经验:曾经有一次交易所升级,所有连接同时断开。如果没有随机抖动,几千个客户端同时重连,直接把交易所的网关打崩了。从那以后,我每次做重连策略都会加上抖动。
好了,WebSocket连接管理这块就聊到这儿。记住三个关键词:多路复用省连接,心跳保活不断线,指数退避稳重连。把这套东西搭好了,你的订单流系统就稳了一半。