WebSocket心跳检测机制的实战踩坑与优化方案

迷人的慧玲 交互 阅读 939
赞 15 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

最近做了一个实时监控系统,需要保持客户端和服务端的长连接状态。之前都是用轮询的方式,但项目数据量比较大,轮询的性能消耗实在太大了。产品经理还一直在催着要实时性更好的方案,没办法,只能研究心跳检测了。

WebSocket心跳检测机制的实战踩坑与优化方案

说白了就是客户端定期向服务端发送一个很小的数据包,确认连接是否正常。如果服务端没收到心跳,或者客户端收不到回应,那就说明连接断了,需要重连。听起来挺简单的,但实际做的时候发现坑不少。

基本实现思路

刚开始的想法很简单,用WebSocket建立长连接,然后定时发送心跳包就行了。代码大概长这样:

class HeartbeatClient {
  constructor(url) {
    this.url = url;
    this.ws = null;
    this.heartbeatTimer = null;
    this.reconnectTimer = null;
    this.isManualClose = false;
  }

  connect() {
    this.ws = new WebSocket(this.url);
    
    this.ws.onopen = () => {
      console.log('连接已建立');
      this.startHeartbeat();
      this.resetReconnectTimer();
    };

    this.ws.onmessage = (event) => {
      const data = JSON.parse(event.data);
      // 收到服务端的心跳回应
      if (data.type === 'pong') {
        this.lastPongTime = Date.now();
      }
    };

    this.ws.onerror = (error) => {
      console.error('WebSocket错误:', error);
      this.handleDisconnect();
    };

    this.ws.onclose = () => {
      console.log('连接已关闭');
      this.stopHeartbeat();
      if (!this.isManualClose) {
        this.scheduleReconnect();
      }
    };
  }

  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.ws.send(JSON.stringify({ type: 'ping' }));
      }
    }, 30000); // 30秒发一次心跳
    
    this.lastPongTime = Date.now();
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer);
      this.heartbeatTimer = null;
    }
  }

  handleDisconnect() {
    this.stopHeartbeat();
    if (!this.isManualClose) {
      this.scheduleReconnect();
    }
  }

  scheduleReconnect() {
    this.reconnectTimer = setTimeout(() => {
      console.log('尝试重连...');
      this.connect();
    }, 5000); // 5秒后重连
  }

  resetReconnectTimer() {
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
  }
}

看起来挺完美的,本地测试也没问题。但是部署到生产环境后就开始出各种问题了。

最大坑:网络不稳定导致的误判

最头疼的问题是网络不稳定会导致频繁断线重连。有一次服务器日志显示,明明客户端连接正常,但就是收不到心跳回应,结果客户端自己把连接断了重新连。查了半天才发现,某些网络环境下心跳包会有延迟甚至丢失。

开始没想到这个问题会这么严重,后来加了一些监控发现,心跳超时的次数远比预期多。用户那边经常看到连接状态在”连接中”和”重连中”之间频繁切换,体验特别差。

后来调整了方案,加了超时判断和重试机制:

class ImprovedHeartbeatClient extends HeartbeatClient {
  constructor(url) {
    super(url);
    this.heartbeatTimeout = null;
    this.pingInterval = 30000; // 心跳间隔
    this.timeoutThreshold = 60000; // 超时阈值
    this.maxRetryTimes = 3; // 最大重试次数
    this.retryCount = 0;
  }

  startHeartbeat() {
    this.stopHeartbeat(); // 防止重复启动
    
    this.heartbeatTimer = setInterval(() => {
      if (this.ws && this.ws.readyState === WebSocket.OPEN) {
        this.sendPing();
      }
    }, this.pingInterval);
    
    this.lastPongTime = Date.now();
  }

  sendPing() {
    // 先检查上次pong的时间,避免重复发送
    const now = Date.now();
    if (now - this.lastPongTime > this.timeoutThreshold) {
      // 已经超时了,直接断开重连
      this.handleTimeout();
      return;
    }

    try {
      this.ws.send(JSON.stringify({ 
        type: 'ping', 
        timestamp: Date.now() 
      }));
      
      // 设置心跳超时检测
      this.heartbeatTimeout = setTimeout(() => {
        this.handlePongTimeout();
      }, 5000); // 5秒内没收到pong就认为超时
      
    } catch (error) {
      console.error('发送心跳失败:', error);
      this.handleDisconnect();
    }
  }

  handlePongTimeout() {
    console.warn('心跳超时,进行重试');
    
    if (this.retryCount < this.maxRetryTimes) {
      this.retryCount++;
      console.log(重试第${this.retryCount}次);
      // 延迟一下再重试,避免频繁重试
      setTimeout(() => {
        if (this.ws && this.ws.readyState === WebSocket.OPEN) {
          this.sendPing();
        }
      }, 2000);
    } else {
      // 重试次数用完了,直接断开重连
      console.log('心跳超时次数过多,断开重连');
      this.handleTimeout();
    }
  }

  handleTimeout() {
    this.retryCount = 0;
    if (this.ws) {
      this.isManualClose = true;
      this.ws.close();
    }
  }

  onMessage(event) {
    const data = JSON.parse(event.data);
    if (data.type === 'pong') {
      this.lastPongTime = Date.now();
      this.retryCount = 0; // 重置重试次数
      
      if (this.heartbeatTimeout) {
        clearTimeout(this.heartbeatTimeout);
        this.heartbeatTimeout = null;
      }
    } else {
      // 处理其他消息
      this.handleOtherMessage(data);
    }
  }

  stopHeartbeat() {
    super.stopHeartbeat();
    if (this.heartbeatTimeout) {
      clearTimeout(this.heartbeatTimeout);
      this.heartbeatTimeout = null;
    }
    this.retryCount = 0;
  }
}

服务端的配合

光客户端改还不够,服务端也得配合。服务端需要响应客户端的心跳,并且也要检测客户端是否还在连接:

// Node.js WebSocket服务端示例
const WebSocket = require('ws');

const wss = new WebSocket.Server({ port: 8080 });

wss.on('connection', (ws) => {
  let isAlive = true;
  
  // 监听客户端心跳
  ws.on('message', (data) => {
    try {
      const message = JSON.parse(data);
      if (message.type === 'ping') {
        // 回复pong
        ws.send(JSON.stringify({
          type: 'pong',
          timestamp: Date.now()
        }));
        
        isAlive = true; // 收到心跳,标记为活跃
      }
    } catch (error) {
      console.error('解析消息失败:', error);
    }
  });

  // 定期检测客户端是否还在线
  const interval = setInterval(() => {
    if (!isAlive) {
      console.log('客户端无响应,关闭连接');
      ws.terminate();
      return;
    }
    
    isAlive = false; // 等待下次心跳
  }, 45000); // 比客户端心跳间隔长一点

  ws.on('close', () => {
    clearInterval(interval);
  });
});

移动端兼容性问题

还有一个问题差点把我搞疯,就是移动端浏览器的休眠机制。手机锁屏一段时间后,JavaScript会被暂停执行,心跳就停了。等用户解锁时,连接早就断了,但客户端还不知道,要等很久才会触发onclose事件。

最后加了个页面可见性检测:

// 页面可见性变化处理
document.addEventListener('visibilitychange', () => {
  if (document.visibilityState === 'visible') {
    // 页面重新可见,检查连接状态
    if (this.ws && this.ws.readyState !== WebSocket.OPEN) {
      console.log('页面恢复可见,尝试重连');
      this.connect();
    }
  }
});

// 页面即将隐藏时的处理
window.addEventListener('pagehide', () => {
  this.isManualClose = true;
  if (this.ws) {
    this.ws.close();
  }
});

最终效果评估

经过这些调整,线上稳定多了。心跳检测现在能比较准确地判断连接状态,误判率降低了90%以上。重连机制也比较可靠,大部分情况下都能自动恢复连接。

不过还是有一些小问题没完全解决。比如极端网络环境下偶尔还会出现连接状态不一致的情况,但概率已经很小了,影响不大。还有就是心跳检测本身也会消耗一些网络流量,虽然不多,但也是个考虑因素。

性能方面倒是没问题,心跳包都很小,一般只有几十个字节,对整体性能影响可以忽略不计。内存占用也控制得很好,几个定时器不会造成内存泄漏。

一些优化建议

根据这次的经验,给后面做类似功能的朋友一些建议:

  • 心跳间隔不能设置得太短,否则会增加服务器压力,也不能太长,否则故障发现不够及时
  • 超时时间要合理设置,要考虑网络延迟和重试机制
  • 移动端要考虑页面生命周期的影响
  • 最好加上详细的日志记录,方便排查问题

另外,如果项目对实时性要求不是特别高的话,其实轮询也不是不能用。心跳检测确实复杂一些,但实时性确实比轮询好太多了。

以上是我踩坑后的总结,希望对你有帮助。这个功能看起来简单,但要做稳定真的不容易。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论