WebSocket断线重连与消息去重的客户端设计
实时通知一旦遇到地铁、电梯或切换网络,WebSocket 客户端通常会同时暴露两个问题:连接断了却没有恢复,或者恢复后同一条业务消息被渲染两遍。可靠的做法不是在 close 回调里无条件新建连接,而是把连接代次、退避计时器、消息标识和待发送队列放进同一个客户端控制器。收到消息后先去重,再修改页面状态;涉及写入、扣款等副作用时,还要把同一个请求标识交给服务端做幂等处理。
- 每次连接都绑定一个代次,旧连接的异步事件不能修改新连接状态。
messageId适合挡重复到达,seq适合发现乱序或缺口,两者用途不同。- 重连使用指数退避加随机抖动,待发送队列必须有容量、过期和失败策略。
- 客户端去重只保护当前页面,服务端仍要用
requestId保证真正的业务幂等。
先把 WebSocket 连接状态收进一个控制器
浏览器的 WebSocket 提供 open、message、error 和 close 事件。实际项目中最容易出错的是旧连接晚到的 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 事件只负责调度恢复;如果用户主动退出,应先设置关闭标记,避免把正常退出重新连回来。

messageId 和 seq 要解决不同的重复问题
messageId 解决的是“这一条消息是否已经处理过”。客户端可以保留一个有上限的 Map,超过时间窗口或容量后淘汰旧标识,不能无限增长。若服务端消息带有递增 seq,则可以额外记录 lastSeq:小于等于已确认序号的消息直接忽略,大于下一序号的消息标记为存在缺口,再决定请求补发还是刷新快照。
不要把“收到过同样内容”当作去重依据。两次文本相同的库存更新可能发生在不同时间,稳定的消息 ID 才能表达一次事件。相反,如果服务端重连后会重新发送最近一段消息,seq 还能帮助客户端判断消息是否乱序,但它并不能替代跨分区生成的唯一 ID。

断线期间的发送队列要有边界
实时客户端不应把所有离线操作都无限缓存。通知类消息可以直接丢弃或只保留最后一条;用户明确提交的操作则应携带 requestId,进入有限队列,连接恢复后再发送。队列项至少记录创建时间、操作类型和重试次数,超过有效期就提示用户,而不是静默重复提交。
| 数据类型 | 客户端策略 | 服务端要求 |
|---|---|---|
| 状态通知 | 按 messageId 去重,缺口时拉快照 | 消息可重放,快照有版本 |
| 用户操作 | 有限队列,过期需提示 | requestId 幂等,返回最终结果 |
| 输入事件 | 通常只保留最新值 | 允许覆盖或按版本拒绝旧值 |
退避参数也应可观测:记录当前连接代次、重连次数、队列长度、最近一次 close code 和去重丢弃数。这样遇到“页面看起来卡住”时,能先判断是网络未恢复、消息缺口,还是客户端因为重复保护丢弃了本应处理的新事件。
常见问题
客户端去重后还会不会重复扣款?
会。客户端刷新、多个标签页或请求已到达服务端但响应丢失时,仍可能再次提交。扣款、下单等副作用必须在服务端以 requestId 做幂等。
为什么重连要加随机抖动?
固定延迟会让同时断线的客户端在同一时刻集中连接。指数退避降低频率,随机抖动则把请求摊开,减少恢复瞬间的连接尖峰。
只保存最后一个 seq 是否足够?
只适合服务端保证连续序号且客户端允许缺口时。需要严格顺序时要记录缺口范围,必要时拉取快照或补发,不能把大于下一序号的消息直接当成正常更新。
Go sort.Slice比较器不满足严格弱序时的异常表现
- 上一篇
- Go sort.Slice比较器不满足严格弱序时的异常表现
- 下一篇
- Go flag.FlagSet构建独立子命令解析器的组织方式
-
- 文章 · 前端 | 48分钟前 | 前端 · 缓存 · Service Worker CacheStorage 缓存版本 activate
- Service Worker缓存版本切换与旧缓存清理流程
- 490浏览 收藏
-
- 文章 · 前端 | 2天前 |
- View Transitions在路由切换中保留关键元素连续性
- 305浏览 收藏
-
- 文章 · 前端 | 2天前 |
- CSS :has选择器实现父级状态样式的边界
- 252浏览 收藏
-
- 文章 · 前端 | 3天前 |
- CSS container queries按组件宽度适配卡片布局
- 367浏览 收藏
-
- 文章 · 前端 | 3天前 | 前端 · 性能优化 · javascript · ArrayBuffer postMessage TypedArray 结构化克隆 Web Worker Transferable
- Web Worker传递大数组时选择Transferable降低复制
- 140浏览 收藏
-
- 文章 · 前端 | 3天前 |
- IndexedDB事务生命周期内完成读写操作的结构
- 176浏览 收藏
-
- 文章 · 前端 | 3天前 | 前端 · javascript · Fetch AbortController AbortSignal 前端请求取消
- AbortController取消搜索请求并避免旧结果覆盖新结果
- 242浏览 收藏
-
- 文章 · 前端 | 3天前 |
- Fetch API流式读取响应并显示下载进度的实现
- 359浏览 收藏
-
- 文章 · 前端 | 3天前 | localStorage 前端存储 Web Storage 配额异常
- Web Storage处理 localStorage 配额异常的实现方法
- 308浏览 收藏
-
- 文章 · 前端 | 3天前 | 前端 · css · CSS :focus-visible 键盘焦点 Web Accessibility
- Web Accessibility保留键盘焦点而不干扰鼠标样式的实现方法
- 143浏览 收藏
-
- 文章 · 前端 | 3天前 |
- ResizeObserver避免尺寸回调触发布局循环的实现方法
- 209浏览 收藏
-
- 文章 · 前端 | 3天前 | 前端 · 性能优化 · javascript · IntersectionObserver rootMargin threshold 列表懒加载
- IntersectionObserver实现列表懒加载的阈值策略的实现方法
- 360浏览 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 485次学习
-
- PubMedQA
- 深入了解PubMedQA生物医学问答数据集,涵盖其核心功能、使用方法及在临床决策、药物研发等场景的应用,助力提升NLP模型性能。
- 187次使用
-
- H2O EvalGPT
- H2O EvalGPT是H2O.ai推出的开源LLM评估平台,提供详细的大模型性能排行榜、行业特定基准测试及A/B测试功能,助您快速选择最适合项目的高性能大语言模型。
- 243次使用
-
- LMArena
- LMArena是加州大学伯克利分校推出的AI模型匿名评测平台。通过盲测投票机制,用户可对比不同大模型回答并生成实时排行榜,助力开发者优化模型及用户选择最佳AI工具。
- 201次使用
-
- HELM
- 深入了解斯坦福推出的HELM(Holistic Evaluation of Language Models)大模型评测体系。本文解析其核心功能、安装配置步骤及应用场景,涵盖准确性、公平性、鲁棒性等多维度指标,助力开发者全面优化语言模型性能。
- 183次使用
-
- CMMLU
- 深入了解CMMLU中文评估基准,涵盖67个学科主题,提供数据集下载、Zero-shot/Five-shot评估方法及排行榜,助力优化中文语言模型性能。
- 172次使用
-
- Go语言对前端领域的入侵WebAssembly运行原理
- 2022-12-31 130浏览
-
- golang基于websocket通信tcpkeepalive研究记录
- 2022-12-23 311浏览
-
- golang实现一个简单的websocket聊天室功能
- 2022-12-28 447浏览
-
- Golang使用WebSocket通信的实现
- 2022-12-26 493浏览
-
- 使用Go语言创建WebSocket服务的实现示例
- 2022-12-28 156浏览

