登录
推荐 文章 Go 技术 课程 下载 专题 AI
首页 >  文章 >  前端

WebSocket 断线重连如何避免多个定时器叠加

来源:17golang原创

时间:2026-09-12 18:00:49 205浏览 收藏

WebSocket 断线重连要避免多个定时器叠加,关键不是把 setTimeout 的时间调大,而是让所有断线入口只调用一个调度函数,并在函数内部用 reconnectTimer 做幂等保护。实际实现中只由 close 事件安排下一次连接,error 负责记录原因;连接成功后清零重试次数,停止组件时同时清理计时器和旧连接。

要点速览
  • 一个控制器只保留一个待执行的重连计时器。
  • 等待时间按指数增长,并设置最大值和轻微随机抖动。
  • 旧 socket 的回调要被忽略,页面销毁时必须 clearTimeout。

先把 WebSocket 重连问题拆成三个状态

浏览器的 WebSocket 对象通过 readyState 表示连接状态,open 表示连接建立,close 表示连接已经关闭,error 表示发生了连接错误。最容易出错的写法是在 onerroronclose 中分别调用重连函数:一次网络故障可能同时经过两个回调,于是每次失败都会多出一条定时器链。

可以先明确三个状态:当前 socket、当前重试次数、当前待执行的计时器。它们由同一个控制器拥有,其他业务代码只调用 startstop,不直接创建定时器。

把重连责任集中在 close 事件

下面的控制器把“创建连接”和“安排重连”分开。error 不再触发第二次重试,真正的调度入口只有 closescheduleReconnect 开头的计时器判断,则让重复的关闭通知变成无操作。

class ReconnectingSocket {
  constructor(url) {
    this.url = url;
    this.socket = null;
    this.reconnectTimer = null;
    this.retryCount = 0;
    this.stopped = false;
  }

  start() {
    // 重新启动前清除停止标记;真正的连接只从这里创建。
    this.stopped = false;
    this.connect();
  }

  connect() {
    if (this.stopped) return;
    const current = new WebSocket(this.url);
    this.socket = current;

    current.addEventListener("open", () => {
      // 成功后恢复首轮等待,避免下一次断线直接使用长延迟。
      if (current !== this.socket) return;
      this.retryCount = 0;
      console.info("WebSocket 已连接");
    });

    current.addEventListener("error", () => {
      // error 只保留诊断职责;close 事件会负责唯一一次调度。
      console.warn("WebSocket 连接发生错误");
    });

    current.addEventListener("close", () => {
      // 忽略旧连接的回调,避免它改写新连接的状态。
      if (current !== this.socket || this.stopped) return;
      this.scheduleReconnect();
    });
  }

  scheduleReconnect() {
    // 已经有计时器时直接返回,保证全局只有一个待执行任务。
    if (this.reconnectTimer !== null || this.stopped) return;
    const base = Math.min(1000 * 2 ** this.retryCount, 30000);
    const jitter = Math.floor(Math.random() * 500);
    const delay = base + jitter;
    this.retryCount += 1;
    console.info(`第 ${this.retryCount} 次重连,等待 ${delay}ms`);
    this.reconnectTimer = window.setTimeout(() => {
      // 回调开始就归还计时器槽位,下一次失败才能重新预约。
      this.reconnectTimer = null;
      this.connect();
    }, delay);
  }

  stop() {
    // 页面销毁或用户退出时,计时器和连接都必须一起收口。
    this.stopped = true;
    if (this.reconnectTimer !== null) {
      window.clearTimeout(this.reconnectTimer);
      this.reconnectTimer = null;
    }
    if (this.socket) {
      this.socket.close(1000, "主动停止");
      this.socket = null;
    }
  }
}
WebSocket 连接、重连调度器和停止清理之间的静态关系示意图
图1:操作示意图,查看连接状态、唯一重连调度器与停止清理边界之间的关系。

指数退避要有上限,也要能停止

1000 * 2 ** retryCount 让连续失败的请求逐步拉开间隔,30000 是等待上限,随机的 jitter 可以避免多个客户端在同一时刻同时冲击服务端。上限不是越大越好:实时看板通常可以接受几十秒恢复,但应根据业务允许的离线时长调整。

场景推荐处理原因
首次断线短延迟重试快速恢复临时网络抖动
连续失败指数增长并封顶减少无效连接请求
连接成功retryCount 清零下一次故障从短延迟开始
页面离开clearTimeout 后 close不让后台回调重新建连

这里的计时器只是“未来某个时刻尝试一次”的预约,不代表连接已经恢复。只有 open 回调确认当前 socket 仍是控制器持有的对象,才可以把重试次数归零。

WebSocket 指数退避参数与连接生命周期的静态边界示意图
图2:结果示意图,展示退避参数、连接生命周期和停止入口如何共同约束重连调度。

检查旧回调、重复启动和页面销毁

如果旧 socket 已经关闭,而新 socket 正在连接,旧对象稍晚到达的 close 回调不能再次预约计时器,所以示例用 current !== this.socket 做对象身份判断。应用层也要避免重复调用 start;如果确实需要“重置连接”,先调用 stop,再启动新的控制器。

日志至少保留重试次数、等待毫秒数和当前 readyState。当同一时刻出现两条“第 N 次重连”日志,或一个控制器同时拥有两个非空计时器引用,就应回到事件绑定处排查是否还有隐藏的 setInterval、组件重复挂载或 error 回调重试。

常见问题

为什么不建议在 error 和 close 中都重连?

它们可能由同一次连接故障连续触发,两个入口会各自预约任务。让 error 只记录诊断,让 close 统一调度,职责更容易保持唯一。

重连定时器已经 clearTimeout,为什么还会重新连接?

通常是回调已经进入事件循环,或者旧 socket 的 close 回调没有做身份判断。清理计时器之外,还要设置 stopped,并忽略不再属于当前控制器的 socket 回调。

参考资料:MDN WebSocket API、WebSocket close 事件与 WHATWG WebSockets 规范。示意图为原创静态关系图,不代表本机真实运行截图。

声明:本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
相关阅读
更多>
最新阅读
更多>
课程推荐
更多>