WebSocket 为什么会假死?一次 Chrome 扩展里的真实踩坑

66 阅读3分钟

WebSocket 管理

Websocket 管理模块其实是整个项目的核心部分,主要用来获取交易所实时信息,这部分采用了单例模式,便于全局统一管理维护 WebSocket,避免多实例连接、状态不一致和重复重连的问题。

由于 WebSocket 是运行在Chrome MV3background Service Worker中,不能保证长期常驻,同时还要把数据传递到popupcontentUI 层中,做了一层封装,统一处理连接管理、状态判断和异常恢复。

demo1.png

1、基础功能

  • connect(): 初始连接
  • disconnect(): 主动断开连接
  • onMessage(): 消息处理回调
  • onOpen(): 连接成功回调
  • onClose(): 连接关闭回调
  • onError(): 连接错误回调

2、指数退避重连策略

问题:

  • 在 MV3 下 background 是 Service Worker,本身不保证长期常驻,网络切换或者空闲回收都可能导致 WebSocket 中断;
  • 在网络不稳定或交易所限流场景下,如果立即重连话,很容易产生连接风暴问题。

方案:采用指数退避重连策略来防止出现以上情况。这样就可以在网络短暂抖动(比如:切换网络),可以快速恢复 Websocket 连接,而在持续异常时,主动降频,减少资源消耗和降低被风控风险。

  • scheduleRetry():
  /**
   * 调度重试连接
   */
  private scheduleRetry(): void {
    // 主动断开,不重试
    if (this.isManualDisconnect) {
      console.log('[WsManager] 主动断开,不进行重试');
      return;
    }

    // 没有连接信息,无法重试
    if (!this.currentExchange || !this.currentTokenList.length) {
      console.log('[WsManager] 没有连接信息,无法重试');
      return;
    }

    // 先检查是否已经在冷却模式
    if (this.inCooldownMode) {
      console.log('[WsManager] 已在冷却模式,跳过重试');
      return;
    }

    // 增加重试计数
    this.retryCount++;

    // 检查是否超过最大重试次数,进入冷却模式
    if (this.retryCount > this.config.maxRetries) {
      console.log(`[WsManager] 已达到最大重试次数 (${this.config.maxRetries}),进入冷却模式`);
      this.enterCooldownMode();
      return;
    }

    const delay = this.getRetryDelay();
    console.log(`[WsManager] 第 ${this.retryCount}/${this.config.maxRetries} 次重试,${delay / 1000}秒后执行...`);

    this.clearRetryTimer();
    this.retryTimer = setTimeout(() => {
      console.log(`[WsManager] 开始第 ${this.retryCount} 次重连...`);
      this.isManualDisconnect = false; // 确保重连时允许后续重试
      this.connect(this.currentExchange!, this.currentTokenList);
    }, delay);
  }

3、假死检测(连接存在但无数据)

问题:在使用 WebSocket 的过程中,我遇到一个比较隐蔽的问题:有时把电脑息屏、浏览器进入后台或切换网络后,WebSocket 连接状态仍然后 OPEN 状态,但实际上已经无法接收到任何数据,而且也没有触发closeerror回调,导致页面上网络状态一直显示正常,但是数据却没有变化。

原因:在浏览器空闲或后台状态下,浏览器内部会主动降级或挂起长连接的调度,但并不一定触发 WebSocket 的回调,从而导致“假死”状态。

方案:如果仅通过 WebSocket 状态来判断连接是否正常,就会出现上面的问题;所以这里我又基于 WebSocket 是否持续接收到真实行情数据,来判断 WebSocket 是否处于健康状态。一旦在设定时间内没有收到有效的数据,就判定为假死状态进而可以触发重连策略。避免页面长期处于“看似正常、实际不可用”的状态。

  • detectAndHandleStaleConnection()
  /**
   * 检测并处理 WebSocket 假死状态
   * 如果连接存在但长时间没有收到消息,则更新状态为 OFFLINE 并强制重连
   * @param staleThreshold 假死阈值(毫秒),默认 60000 (1分钟)
   */
  detectAndHandleStaleConnection(staleThreshold: number = 60000): boolean {
    const now = Date.now();
    const timeSinceLastMessage = now - this.lastMessageAt;

    // 如果 WebSocket 显示连接,但超过阈值时间没有收到消息,判定为假死
    if (this.isConnected() && timeSinceLastMessage > staleThreshold) {
      console.warn(`[WsManager] 检测到 WebSocket 假死: ${Math.floor(timeSinceLastMessage / 1000)}秒内无消息`);

      // 更新状态为 OFFLINE
      this.setDataStatus(DataStatus.OFFLINE);
      return true; // 返回 true 表示检测到假死
    }

    return false; // 返回 false 表示连接正常
  }

4、冷却模式(频繁失败后降低重试频率)

冷却模式其实是为了解决网络不可恢复时的兜底方案,比如交易所的 WebSocket API 只有在特定网络下才能使用,比如关闭 VPN 或者断开网络就会无法连接成功。这种情况下,重连次数很快就会被消耗完,而当网络恢复的时候,还需要手动刷新来启动插件。因此设计了冷却模式,当重连次数达到阈值后就会进入冷却模式,直到网络恢复从而自动连接。

  • enterCooldownMode()
 /**
   * 进入冷却模式
   * - 状态变为 OFFLINE
   * - 每隔 cooldownInterval 尝试重连一次
   */
  private enterCooldownMode(): void {
    console.log('this.inCooldownMode', this.inCooldownMode);
    if (this.inCooldownMode) return;

    this.inCooldownMode = true;
    this.setDataStatus(DataStatus.OFFLINE);

    console.log(`[WsManager] 进入冷却模式,每 ${this.config.cooldownInterval / 1000}s 尝试重连一次`);

    // 启动冷却定时器
    this.cooldownTimer = setInterval(() => {
      console.log('[WsManager] 冷却模式:尝试重连...');
      this.attemptCooldownReconnect();
    }, this.config.cooldownInterval);
  }

📝 写在最后

目前这个插件只有我在本地使用,也还可能存在一些 bug。

项目已放在 GitHub 上: github.com/CHYYHC123/c…

release 目录下包含已打包好的 Chrome 扩展,可直接安装体验。 链接:github.com/CHYYHC123/c…

目前的功能设计更多是基于个人使用习惯,如果你在使用过程中有更好的想法、发现问题,或者有不同的设计思路,欢迎一起交流讨论。