当前位置:首页 > 文章列表 > 文章 > 前端 > WebSocket断线重连与消息去重的客户端设计

WebSocket断线重连与消息去重的客户端设计

来源:17golang原创 2026-09-23 13:10:03 0浏览 收藏

实时通知一旦遇到地铁、电梯或切换网络,WebSocket 客户端通常会同时暴露两个问题:连接断了却没有恢复,或者恢复后同一条业务消息被渲染两遍。可靠的做法不是在 close 回调里无条件新建连接,而是把连接代次、退避计时器、消息标识和待发送队列放进同一个客户端控制器。收到消息后先去重,再修改页面状态;涉及写入、扣款等副作用时,还要把同一个请求标识交给服务端做幂等处理。

要点速览
  • 每次连接都绑定一个代次,旧连接的异步事件不能修改新连接状态。
  • messageId 适合挡重复到达,seq 适合发现乱序或缺口,两者用途不同。
  • 重连使用指数退避加随机抖动,待发送队列必须有容量、过期和失败策略。
  • 客户端去重只保护当前页面,服务端仍要用 requestId 保证真正的业务幂等。

先把 WebSocket 连接状态收进一个控制器

浏览器的 WebSocket 提供 openmessageerrorclose 事件。实际项目中最容易出错的是旧连接晚到的 close 覆盖了新连接,以及多个定时器同时发起重连。连接代次可以把这两个问题隔开:创建新 socket 时递增代次,事件回调只接受属于当前代次的实例。

class RealtimeClient {
  constructor(url, onMessage) {
    this.url = url;
    this.onMessage = onMessage;
    this.socket = null;
    this.generation = 0;
    this.retryCount = 0;
    this.retryTimer = null;
    this.seen = new Map();
    this.pending = [];
  }

  connect() {
    const generation = ++this.generation;
    const socket = new WebSocket(this.url);
    this.socket = socket;

    socket.addEventListener("open", () => {
      // 只有当前连接能清零退避计数并冲刷队列。
      if (generation !== this.generation) return;
      this.retryCount = 0;
      this.flushPending();
    });

    socket.addEventListener("message", (event) => {
      // 先校验消息结构和幂等标识,再交给业务状态层。
      if (generation !== this.generation) return;
      const message = JSON.parse(event.data);
      if (!this.acceptOnce(message)) return;
      this.onMessage(message);
    });

    socket.addEventListener("close", () => {
      // 旧连接关闭时不能替新连接安排第二个定时器。
      if (generation === this.generation) this.scheduleReconnect();
    });
  }

  acceptOnce(message) {
    if (!message.messageId) return true;
    if (this.seen.has(message.messageId)) return false;
    this.seen.set(message.messageId, Date.now());
    return true;
  }

  scheduleReconnect() {
    if (this.retryTimer) return;
    const base = Math.min(30000, 500 * 2 ** this.retryCount++);
    const delay = base + Math.floor(Math.random() * 400);
    this.retryTimer = setTimeout(() => {
      this.retryTimer = null;
      this.connect();
    }, delay);
  }

  flushPending() {
    while (this.pending.length && this.socket?.readyState === WebSocket.OPEN) {
      this.socket.send(JSON.stringify(this.pending.shift()));
    }
  }
}

这段代码只展示客户端结构,JSON.parse 在生产代码中还应放进异常处理,并校验消息版本、类型和大小。close 事件只负责调度恢复;如果用户主动退出,应先设置关闭标记,避免把正常退出重新连回来。

WebSocket 客户端连接代次、当前 socket、open close 事件与重连定时器的静态关系说明图
图1:连接代次把当前 socket、事件回调和重连定时器绑定在同一边界内,是静态关系说明图,不是运行截图。

messageId 和 seq 要解决不同的重复问题

messageId 解决的是“这一条消息是否已经处理过”。客户端可以保留一个有上限的 Map,超过时间窗口或容量后淘汰旧标识,不能无限增长。若服务端消息带有递增 seq,则可以额外记录 lastSeq:小于等于已确认序号的消息直接忽略,大于下一序号的消息标记为存在缺口,再决定请求补发还是刷新快照。

不要把“收到过同样内容”当作去重依据。两次文本相同的库存更新可能发生在不同时间,稳定的消息 ID 才能表达一次事件。相反,如果服务端重连后会重新发送最近一段消息,seq 还能帮助客户端判断消息是否乱序,但它并不能替代跨分区生成的唯一 ID。

WebSocket 消息信封中的 messageId、seq、requestId 与业务状态层的去重边界说明图
图2:消息信封把 messageId、seq、requestId 和业务状态更新分层,说明显示层去重与服务端幂等的关系。

断线期间的发送队列要有边界

实时客户端不应把所有离线操作都无限缓存。通知类消息可以直接丢弃或只保留最后一条;用户明确提交的操作则应携带 requestId,进入有限队列,连接恢复后再发送。队列项至少记录创建时间、操作类型和重试次数,超过有效期就提示用户,而不是静默重复提交。

数据类型客户端策略服务端要求
状态通知按 messageId 去重,缺口时拉快照消息可重放,快照有版本
用户操作有限队列,过期需提示requestId 幂等,返回最终结果
输入事件通常只保留最新值允许覆盖或按版本拒绝旧值

退避参数也应可观测:记录当前连接代次、重连次数、队列长度、最近一次 close code 和去重丢弃数。这样遇到“页面看起来卡住”时,能先判断是网络未恢复、消息缺口,还是客户端因为重复保护丢弃了本应处理的新事件。

常见问题

客户端去重后还会不会重复扣款?

会。客户端刷新、多个标签页或请求已到达服务端但响应丢失时,仍可能再次提交。扣款、下单等副作用必须在服务端以 requestId 做幂等。

为什么重连要加随机抖动?

固定延迟会让同时断线的客户端在同一时刻集中连接。指数退避降低频率,随机抖动则把请求摊开,减少恢复瞬间的连接尖峰。

只保存最后一个 seq 是否足够?

只适合服务端保证连续序号且客户端允许缺口时。需要严格顺序时要记录缺口范围,必要时拉取快照或补发,不能把大于下一序号的消息直接当成正常更新。

版本声明
本文转载于:17golang原创 如有侵犯,请联系study_golang@163.com删除
Go sort.Slice比较器不满足严格弱序时的异常表现Go sort.Slice比较器不满足严格弱序时的异常表现
上一篇
Go sort.Slice比较器不满足严格弱序时的异常表现
Go flag.FlagSet构建独立子命令解析器的组织方式
下一篇
Go flag.FlagSet构建独立子命令解析器的组织方式
查看更多
最新文章
查看更多
课程推荐
  • 前端进阶之JavaScript设计模式
    前端进阶之JavaScript设计模式
    设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
    543次学习
  • GO语言核心编程课程
    GO语言核心编程课程
    本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
    516次学习
  • 简单聊聊mysql8与网络通信
    简单聊聊mysql8与网络通信
    如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
    500次学习
  • JavaScript正则表达式基础与实战
    JavaScript正则表达式基础与实战
    在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
    487次学习
  • 从零制作响应式网站—Grid布局
    从零制作响应式网站—Grid布局
    本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
    485次学习
查看更多
AI推荐
  • PubMedQA数据集详解:生物医学问答基准、功能与应用指南
    PubMedQA
    深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
    187次使用
  • H2O EvalGPT:开源LLM大模型评估与排行榜工具
    H2O EvalGPT
    H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
    243次使用
  • LMArena是什么?伯克利AI模型评估平台使用指南与功能解析
    LMArena
    LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
    201次使用
  • 斯坦福HELM:大语言模型Holistic Evaluation整体评估框架详解
    HELM
    深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
    183次使用
  • CMMLU中文大模型评估基准:功能、使用教程与应用场景解析
    CMMLU
    深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
    172次使用
微信登录更方便
  • 密码登录
  • 注册账号
登录即同意 用户协议隐私政策
返回登录
  • 重置密码