WebSocket断线后恢复消息顺序
来源:17golang原创
时间:2026-11-15 17:53:15 450浏览 收藏
WebSocket 断线后想恢复消息顺序,关键不是让浏览器的 WebSocket 对象“记住”上一条消息,而是给业务消息增加可比较的应用层序号。客户端持久化最后一个已经应用的 sequence,重连时把它作为游标发给服务端;服务端先回放游标之后的缺口,再把实时流交给客户端。客户端还要按序号去重,并在回放追平前暂存实时消息。
RFC 6455 约束的是一条连接上的消息和分片顺序。连接断开以后,新的连接不会自动知道旧连接哪些消息已经送达,也不会替应用补发断线期间的事件。因此,顺序恢复属于应用层协议设计。
先定义顺序契约:WebSocket 有序不等于断线可恢复
可以把每条业务事件表示成下面的对象:
{
// sequence 是服务端为同一业务流单调递增的应用层序号
sequence: 101,
stream: "orders:user-42",
type: "order.updated",
payload: { status: "paid" }
}
这里至少要约定三件事:
- 同一条业务流的
sequence单调递增,不能用客户端收到消息的时间代替。 - 客户端只有在业务状态真正应用成功后,才推进
lastAppliedSeq。 - 回放接口和 WebSocket 实时推送使用同一套序号,不要各自从 1 开始计数。

例如客户端最后应用到 100,重连后请求 after=100,服务端应返回 101、102、103。这样客户端恢复的是一个明确的序号区间,而不是“从大概断开的时间点继续推送”。
保存最后游标:只在应用成功后推进
游标可以放在内存中,也可以按业务需要放到本地存储。重要的是推进时机:收到消息不代表状态已经应用,解析失败、版本不兼容或业务更新失败时都不能提前确认。
const cursorKey = "orders:last-applied-seq";
let lastAppliedSeq = Number(localStorage.getItem(cursorKey) || 0);
function applyEvent(event) {
// 小于等于游标的事件已经处理过,重连回放时直接幂等跳过
if (event.sequence
真实项目中,updateOrderStore 还应具备幂等性。只按序号去重适合单一业务流;如果一条事件会更新多个客户端表,服务端也应使用事件 ID 或版本号辅助排查。
重连后请求回放:让服务端明确返回缺口
重连成功后不要立即假设实时推送已经覆盖断线期间的消息。可以先通过 HTTP 回放接口获取缺口,再建立实时订阅;也可以在 WebSocket 握手后的第一条业务消息中发送游标。下面用一个抽象的回放接口表示前一种方式:
async function replayAfter(cursor) {
// 服务端按同一条业务流的 sequence 升序返回缺失事件
const response = await fetch(`/api/orders/events?after=${cursor}`);
if (!response.ok) {
throw new Error(`replay failed: ${response.status}`);
}
const events = await response.json();
// 回放结果仍按序应用,不把数组下标当成业务序号
for (const event of events) {
applyEvent(event);
}
return events.at(-1)?.sequence ?? cursor;
}
服务端必须说明回放保留多久、游标过旧时返回什么,以及回放是否包含边界值。常见约定是返回 sequence > after 的事件;如果游标已经早于事件保留窗口,则返回“需要全量同步”,而不是静默返回一段不完整的数据。
合并回放与实时流:用切换水位避免实时消息插队
最容易出错的场景是:客户端重连后已经开始接收 204、205,但 HTTP 回放还在返回 201、202、203。若两个通道直接同时调用 applyEvent,界面就可能先应用 204,再应用 201。
一个简单的模式是“回放阶段先入缓冲,追平后再切换实时流”。实时消息先进入 liveBuffer;回放完成后按序合并,最后才允许实时消息直接进入状态层:
let recovering = true;
const liveBuffer = new Map();
function onLiveMessage(event) {
// 回放未追平时先按 sequence 暂存,防止实时消息插队
if (recovering) {
liveBuffer.set(event.sequence, event);
return;
}
applyIfNext(event);
}
function applyIfNext(event) {
// 只接受下一个连续序号;旧消息也能安全跳过
if (event.sequence

生产实现通常会把“切换水位”做得更明确:服务端在回放响应中返回一个快照水位 highWatermark,客户端先应用不超过该水位的回放事件,再消费缓冲中大于水位的实时事件。这样比单纯依赖网络先后更容易解释和排错。
出现重复、缺口或游标过旧怎么办
| 现象 | 处理方式 | 不要做什么 |
|---|---|---|
| 重复 sequence | 按 sequence 或 eventId 幂等跳过 | 不要因为重复就把整个连接判为坏连接 |
| 出现 sequence 缺口 | 暂停应用并重新回放缺口 | 不要直接按到达顺序渲染 |
| 游标早于保留窗口 | 请求全量快照,再从快照水位继续 | 不要假装回放成功 |
| 业务应用失败 | 保留旧游标,记录事件并重试或进入人工补偿 | 不要先写入新游标 |
一个可落地的判断清单
如果只需要短连接内的顺序,WebSocket 自身的消息交付语义通常够用;只要需求包含断线补发、页面切后台后追平、多个标签页恢复或服务端重启后的继续消费,就应该把下面四个字段写进应用协议:
- 业务流标识
stream。 - 单调递增的
sequence或可排序的事件版本。 - 客户端最后成功应用的
lastAppliedSeq。 - 回放不可用时的全量同步入口和快照水位。
常见问题
WebSocket 重连后会自动收到断线期间的消息吗?
不会。浏览器会创建新的连接,缺失消息是否可恢复取决于服务端是否保存事件,以及应用协议是否提供回放或同步机制。
只记录客户端收到消息的时间可以吗?
不建议。客户端时间可能漂移,且多个消息可能拥有相同时间戳。使用服务端分配的同一业务流序号更容易判断缺口和去重。
回放接口和实时 WebSocket 必须是两个接口吗?
不必须。它们可以共用一个协议,只要都能表达游标、序号、回放边界和切换水位;分成 HTTP 回放加 WebSocket 实时推送只是更容易拆分实现的一种方案。
-
311 收藏
-
447 收藏
-
493 收藏
-
156 收藏
-
370 收藏
-
237 收藏
-
110 收藏
-
443 收藏
-
187 收藏
-
248 收藏
-
109 收藏
-
282 收藏
-
177 收藏
-
文章 · 前端 | 1小时前 | 前端开发 · 浏览器API · CSS View Transition API view-transition-name view-transition-class 伪元素动画413 收藏
-
文章 · 前端 | 1小时前 | javascript · 前端开发 · 浏览器API · Shadow DOM adoptedStyleSheets CSSStyleSheet replaceSync Constructable Stylesheets500 收藏
-
326 收藏
-
461 收藏
-
- 前端进阶之JavaScript设计模式
- 设计模式是开发人员在软件开发过程中面临一般问题时的解决方案,代表了最佳的实践。本课程的主打内容包括JS常见设计模式以及具体应用场景,打造一站式知识长龙服务,适合有JS基础的同学学习。
- 立即学习 543次学习
-
- GO语言核心编程课程
- 本课程采用真实案例,全面具体可落地,从理论到实践,一步一步将GO核心编程技术、编程思想、底层实现融会贯通,使学习者贴近时代脉搏,做IT互联网时代的弄潮儿。
- 立即学习 516次学习
-
- 简单聊聊mysql8与网络通信
- 如有问题加微信:Le-studyg;在课程中,我们将首先介绍MySQL8的新特性,包括性能优化、安全增强、新数据类型等,帮助学生快速熟悉MySQL8的最新功能。接着,我们将深入解析MySQL的网络通信机制,包括协议、连接管理、数据传输等,让
- 立即学习 500次学习
-
- JavaScript正则表达式基础与实战
- 在任何一门编程语言中,正则表达式,都是一项重要的知识,它提供了高效的字符串匹配与捕获机制,可以极大的简化程序设计。
- 立即学习 487次学习
-
- 从零制作响应式网站—Grid布局
- 本系列教程将展示从零制作一个假想的网络科技公司官网,分为导航,轮播,关于我们,成功案例,服务流程,团队介绍,数据部分,公司动态,底部信息等内容区块。网站整体采用CSSGrid布局,支持响应式,有流畅过渡和展现动画。
- 立即学习 485次学习