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. 第一次重连失败:等1秒再试
  3. 第二次失败:等2秒
  4. 第三次失败:等4秒
  5. 以此类推:每次翻倍,直到最大间隔(比如60秒)
  6. 加上随机抖动:在间隔基础上±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连接管理的完整流程。你可以看到多路复用、心跳保活、断线重连三者是如何协同工作的。

WebSocket连接管理架构图 交易所A 交易所B 交易所C 连接管理器 多路复用 心跳保活 断线重连 订阅管理 消息分发 数据处理器 订单簿更新 成交数据 K线数据 账户变动 核心流程: 1. 连接管理器维护与交易所的WebSocket连接 2. 多路复用:一条连接承载多个订阅频道 3. 心跳保活:定时发送心跳,检测连接健康状态 4. 断线重连:指数退避策略,自动恢复连接

3.5 避坑指南

最后分享几个我踩过的坑:

  • 不要忽略WebSocket的close事件:有些同学只监听error事件,忽略了close。实际上close才是断线的主要信号。
  • 重连时清理旧资源:旧的心跳定时器、订阅回调都要清理干净,否则会出现内存泄漏。
  • 做好日志记录:每次断线、重连、心跳超时都要打日志。线上排查问题时,这些日志就是你的救命稻草。
  • 考虑限流:重连太频繁会被交易所封IP。我建议加上全局的限流器,比如每分钟最多重连5次。

我的经验:曾经有一次交易所升级,所有连接同时断开。如果没有随机抖动,几千个客户端同时重连,直接把交易所的网关打崩了。从那以后,我每次做重连策略都会加上抖动。

好了,WebSocket连接管理这块就聊到这儿。记住三个关键词:多路复用省连接,心跳保活不断线,指数退避稳重连。把这套东西搭好了,你的订单流系统就稳了一半。