WebSocket 心跳机制与断线重连

场景题: “现有的系统使用 WebSocket 进行实时的即时通讯(IM)或者大屏数据大盘的毫秒级推送。 但是在弱网(比如进电梯、换网络环境)或者服务端 Nginx 为了安全强制断掉长久无行动的连接时,系统的 WebScoket 会无声无息地‘死去’。 请问怎么用纯 JS 实现一套具有极强鲁棒性的 WebSocket 心跳检测及重型断线重连能力?”

遭遇“假死沉寂”危机

如果我们直接调用 new WebSocket(url) 且把业务写死绑定在 onmessage 上,很容易遭遇一种情况:“前端看着连接建立依然在握手着没报错,后端其实在几小时前早已经由于 TCP 等原因死掉了”,导致数据假沉寂,业务崩塌。这就是为什么必须做心跳检测。

核心实现设计机制

架构实现中,我们将封装这个类为拥有两副重型铠甲的生命体:跳动的源泉(心跳探针)不屈的重生(断网重试)

Phase 1:心跳检测 (Heartbeat)

为了避免“不知不觉地假死”,前端必须养成每隔一段时间就发一句“喂”(Ping),后方一旦活着就秒回一声“哈”(Pong)。

心跳策略机制

  1. 触发定时器:利用特定的时长(例如 30 秒),只要我们长时间没有收到远端的回信我们就在倒计时完毕后发一段如 'ping' 的字符串。
  2. 清脆的一发二响(重定时器):不仅发包,我们在这个时机开启另一颗延时定时雷(如 10 秒后爆炸),如果远端在 10s 后竟然还在沉睡没发回任何含 'pong' 之类的答复,这颗雷就会爆,强行断定为连接死亡,主动触发断开重建生命周期!
  3. 安全触网续命:若是 10s 内对方传来了任何有效信息或者特定的 'pong' 响应,我们一把抹除重定时雷的发生,且刷新心跳时钟从 0 继续等待下一次漫游侦测。

Phase 2:断线重连 (Reconnect)

无论是因为心跳停止还是外部网线遭到物理拔除断裂:系统收到 onclose 或者 onerror 后不能一味地报个 Error 让用户看报错页。它必须开启自动挽救功能。

挽救重连策略机制

  1. 上锁(Lock):不能任由短时间内狂风骤雨地发起成百上千次 new WebSocket。设定 isReconnecting 锁并只允许进入一种连接复原重绘的序列。
  2. 衰退重试 (Exponential Backoff, 指数退避算法):不要像愣头青一样死死地每秒查一次。如果网络被拔了,可以采用一开始等 3 秒;随后失败等 5 秒,再失败拉长时间等 10 秒去连接的退守策略,给服务端喘息的时间避免引发 DDOS 并保护客户端 CPU。最大重复次数设限如 5 次不通过直接彻底宣布连接溃败死亡向屏幕上爆弹窗提示。

可以默写的经典伪骨架代码展示

class ResilientWebSocket {
  constructor(url) {
    this.url = url;
    this.ws = null;
    
    // 断线重连相关配置
    this.isReconnecting = false;  // 防止重连潮汐攻击的锁
    this.reconnectTimer = null;
    this.reconnectAttempts = 0;   // 记录已重连死磕了多少次
    this.maxReconnects = 5;
    
    // 心跳设置配置
    this.pingTimeout = 30000;     // 30s 的心跳安详探视期
    this.pongTimeout = 10000;     // 10s 未收到答复死亡绝斩期
    this.pingTimer = null;
    this.pongTimer = null;

    this.init();
  }

  // 装载实体生命轮回
  init() {
    this.ws = new WebSocket(this.url);

    this.ws.onopen = () => {
      // 一旦顺利搭上桥:清空重试的满身疮痍与锁,且开启心跳心脏仪
      this.reconnectAttempts = 0;
      this.isReconnecting = false;
      this.resetHeartbeat();
      console.log('🔗 WebSocket 连接满血建立!');
    };

    this.ws.onmessage = (event) => {
      // 【核心一】:不管收到的是正常的长消息还是心跳的反馈,都代表对方活着!续命心脏机制。
      this.resetHeartbeat();
      
      if (event.data === 'pong') {
        // 这是心跳的回复,不需要交给真实业务去画屏幕,悄悄消化掉即可
        return;
      }
      
      // ...正式交给外部调用着做 IM 刷新屏幕展示等
    };

    // 监听网络由于 Nginx 强断或者拔除网线
    this.ws.onclose = () => this.reconnect();
    this.ws.onerror = () => this.reconnect();
  }

  // --- 心跳续命引擎 ---
  resetHeartbeat() {
    clearTimeout(this.pingTimer);
    clearTimeout(this.pongTimer);
    
    this.pingTimer = setTimeout(() => {
      if (this.ws.readyState === WebSocket.OPEN) {
        // 探病:发送大吼一声
        this.ws.send('ping');
        
        // 【核心二】:挂上延时炸弹定时雷!过了等待线必死。
        this.pongTimer = setTimeout(() => {
           console.warn('⚠️ 10秒苦等无回应,判定服务端陷入假死状态。强行掐死!');
           // 使用原生方法直接暴力捏死现在的躯壳,会触发后文的 onclose 并联动重连
           this.ws.close(); 
        }, this.pongTimeout);
      }
    }, this.pingTimeout);
  }

  // --- 再度转生引擎 ---
  reconnect() {
    if (this.isReconnecting || this.reconnectAttempts >= this.maxReconnects) {
       console.error('❌ 重连次数已耗尽或者已经在拼命挣扎的路上了,放弃拯救。');
       return;
    }
    this.isReconnecting = true;
    this.reconnectAttempts++;

    // 用指数慢慢后退法来延时抢救时间
    const delay = Math.min(3000 * this.reconnectAttempts, 10000); 
    console.log(`📡 正在准备第 ${this.reconnectAttempts} 次抢救重连,${delay/1000}秒后起搏...`);
    
    this.reconnectTimer = setTimeout(() => {
      this.isReconnecting = false;
      this.init(); // 再次借尸还魂呼叫创建!
    }, delay);
  }
}

面试要义

面对这样一道带有架构味道的工程问题,你的核心展现是展现出对边界网络危机的恐惧,不相信单纯底层 TCP。要说出如何利用 Ping/Pong 把前端的绝对主动握手权牢牢掌控住;并在描述遭遇极端脱网重连时,重点利用“不暴库的 Exponential 延缓时间递增”表现出一个沉淀极其稳重的前端防守底蕴。