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

WebSocket 怎样处理断线后的指数退避重连

来源:17golang原创

时间:2026-11-08 04:27:15 332浏览 收藏

WebSocket 断线后,应由单一入口创建新连接,把连续失败次数转换为有上限的指数等待,并加入随机抖动;主动退出时同时取消定时器和关闭连接。浏览器原生 WebSocket 不会替应用自动完成这些恢复策略,不能在 error 和 close 里各启动一套重连。

标准入口:https://www.rfc-editor.org/rfc/rfc6455

Web API 文档:https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications

故障表现:恢复时连接请求反而变多

考虑一个教学故障场景:服务短暂不可用,一批页面几乎同时断线。第一次实现是在关闭事件里立即重新构造 WebSocket。服务尚未恢复,页面又失败;多个页面不断重复握手,反而增加恢复期间的负担。这里是机制推演,不是某次线上事故或实测数据。

再看局部代码的事件顺序:连接报错,错误回调安排一次重连;随后关闭回调又安排一次。表面上只是“多补一个兜底”,实际却可能留下两个计时器和多个连接实例。根因包含两个独立问题:失败后缺少等待,恢复任务没有唯一所有者。

把失败次数换成有上限的随机等待

RFC 6455 第 7.2.3 节讨论异常关闭恢复,建议先随机延迟,再逐步增加后续尝试的等待时间。工程上可使用截断指数退避:上限 C(n) = min(cap, base × 2^n),n 从第一次失败后的 0 开始。本文选 base 为 500 毫秒、cap 为 30 秒;这些是示例参数,不是协议硬性要求。

为避免所有页面按同一节奏重试,在 [C(n)/2, C(n)) 内随机取整数毫秒。采用这一半区间抖动时,第一次等待约 250–499 毫秒,第二次约 500–999 毫秒;到达上限后仍随机分散。其他抖动算法也可以使用,但前后端需要一致理解速率预算。

参数作用本例取舍
base初始等待尺度500 毫秒,可按服务负载提高
cap增长的最大上限30 秒,防止等待无限增长
jitter分散客户端尝试时间从上限的一半到上限随机选择
maxRetries限制自动恢复次数每个未稳定恢复周期最多 8 次
失败计数基础等待封顶参数随机抖动和重试预算的关系
静态结构图:失败计数、base 和 cap 共同定义等待上限,随机抖动定义实际等待区间,重试预算另行限制尝试数量;不是执行时间轴。

用一个重连入口管理连接和定时器

下面是浏览器端的连接管理骨架。一个管理器对应一个端点,内部维护一个 socket 引用、一个重连定时器和一个稳定性定时器。代码不依赖第三方库,省略了消息处理、鉴权刷新和连接建立超时,接入时需在业务层补充。

function createReconnector(url) {
  let socket = null;
  let retryTimer = null;
  let stableTimer = null;
  let attempts = 0;
  let stopped = false;
  const maxRetries = 8;

  function stop() {
    // 主动退出先禁止重连,再释放两个定时器和连接引用。
    stopped = true;
    clearTimeout(retryTimer);
    clearTimeout(stableTimer);
    retryTimer = stableTimer = null;
    const old = socket;
    socket = null;
    if (old) old.close(1000, "client stop");
  }

  function schedule() {
    // 每个管理器最多只有一个等待中的重连定时器。
    if (stopped || retryTimer !== null) return;
    if (attempts >= maxRetries) {
      stop();
      console.warn("自动重试已耗尽,请人工恢复");
      return;
    }
    const ceiling = Math.min(30_000, 500 * 2 ** attempts);
    attempts += 1;
    // 半区间抖动:等待在指数上限的一半与上限之间。
    const delay = Math.floor(ceiling / 2 + Math.random() * ceiling / 2);
    retryTimer = setTimeout(() => {
      retryTimer = null;
      connect();
    }, delay);
  }

  function connect() {
    if (stopped || socket !== null) return;
    let current;
    try {
      current = new WebSocket(url);
    } catch (error) {
      // URL 等同步配置错误直接停下,不进行无意义的快速重试。
      stopped = true;
      console.error("连接参数错误", error);
      return;
    }
    socket = current;

    current.addEventListener("open", () => {
      if (stopped || socket !== current) return;
      // 连续保持打开十秒才重置;这是示例策略,并非业务就绪证明。
      stableTimer = setTimeout(() => {
        stableTimer = null;
        if (!stopped && socket === current &&
            current.readyState === WebSocket.OPEN) attempts = 0;
      }, 10_000);
    });

    current.addEventListener("error", () => {
      // 网络错误还会触发 close,只在 close 中调度重连。
      if (!stopped && socket === current) console.warn("连接发生错误");
    });

    current.addEventListener("close", (event) => {
      // 旧连接的迟到事件不能控制新连接。
      if (socket !== current) return;
      socket = null;
      clearTimeout(stableTimer);
      stableTimer = null;
      if (stopped) return;
      // 示例约定:正常关闭和策略拒绝不自动重试。
      if (event.code === 1000 || event.code === 1008) {
        stopped = true;
        return;
      }
      schedule();
    });
  }

  connect();
  return { stop };
}
// 页面拥有者调用 createReconnector(自己的 wss 地址),卸载时调用 stop。
// 本示例只管理连接恢复,消息订阅与发送需在业务层另行接入。

error 只记录信息,close 统一决定是否重连。构造器同步抛出的参数错误直接停止,避免把配置问题当成瞬时网络故障。重试次数达到上限后,管理器进入停止状态;用户选择重试时,应在明确的页面操作中创建新的管理器。

端点地址应来自应用自身配置,HTTPS 页面通常使用 wss。不要把鉴权密钥写入日志,也不要向示例占位地址持续发起连接。正文代码是原创教学输入,没有声称已在某个浏览器环境执行。

避免刚连上就重置造成反复快重试

如果在每次 open 事件中立即把失败次数清零,服务“刚连上又断开”时,退避会一直退回最短等待。本例要求连接连续打开十秒才清零;在这十秒内关闭,稳定性定时器被取消,已有失败次数继续保留。

十秒仍只是粗略条件:打开连接不等于已完成业务鉴权,也不等于订阅已恢复。需要更强的恢复判据时,可在业务确认就绪后启动稳定性窗口。不能因为收到任意一条消息,就认定整个会话已经恢复。

连接管理器持有socket重连定时器稳定性定时器与停止标志的结构
静态说明图:管理器统一持有连接和两个定时器;stop 负责释放,连接身份判断隔离旧事件。连线表达所有权和约束,不表示事件先后。

防复发:关闭策略、生命周期与消息恢复

关闭码需要结合应用约定决定。本例不自动重试 1000 正常关闭与 1008 策略拒绝;其他错误也不应一概永久重试。服务自定义的鉴权失败、维护或限额关闭码,应进入相应的刷新凭据、等待或人工恢复路径,而不是反复撞同一个端点。1006 表示异常关闭的观测状态,不能把它当作客户端发出的关闭码。

页面或组件结束使用时调用 stop(),否则后台等待任务可能在组件卸载后创建新连接。页面隐藏不一定代表任务结束,可按产品需求保留或暂停;如果处理 pagehide/pageshow,应明确何时销毁旧管理器、何时建立新实例,避免路由层与页面层各自恢复一次。

重连成功后消息会自动补回来吗? 不会。需要服务端与客户端约定游标、订阅标识或快照恢复;重新发送操作还应处理幂等,不能把连接恢复当作消息恰好送达一次的保证。

定时器到期是否保证立刻发起连接? 不保证,后台标签页、设备休眠与事件循环都会影响触发时间。退避时间是应用请求的等待,不是精确时钟承诺。

网络恢复通知能否直接跳过退避? 可以作为重新评估的信号,但不能据此证明服务可达。若增加该入口,应复用同一个管理器并取消或协调旧定时器,不能再开一条并行重试链。

落地时关注连续失败次数、安排的等待区间、关闭码、当前连接标识及停止原因,并明确重试耗尽后的用户操作。单一所有者解决重复任务,指数等待与抖动控制尝试节奏,业务恢复协议负责补齐断线期间的数据缺口。

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