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

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 开始计数。
服务端事件序号、客户端最后游标和 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 
WebSocket 回放流与实时流经过缓冲、去重和切换水位后按序合并的结构图
图2:结构图展示回放流与实时流通过切换水位合并,并以序号去重后按序应用,不代表某次运行结果。

生产实现通常会把“切换水位”做得更明确:服务端在回放响应中返回一个快照水位 highWatermark,客户端先应用不超过该水位的回放事件,再消费缓冲中大于水位的实时事件。这样比单纯依赖网络先后更容易解释和排错。

出现重复、缺口或游标过旧怎么办

现象处理方式不要做什么
重复 sequence按 sequence 或 eventId 幂等跳过不要因为重复就把整个连接判为坏连接
出现 sequence 缺口暂停应用并重新回放缺口不要直接按到达顺序渲染
游标早于保留窗口请求全量快照,再从快照水位继续不要假装回放成功
业务应用失败保留旧游标,记录事件并重试或进入人工补偿不要先写入新游标

一个可落地的判断清单

如果只需要短连接内的顺序,WebSocket 自身的消息交付语义通常够用;只要需求包含断线补发、页面切后台后追平、多个标签页恢复或服务端重启后的继续消费,就应该把下面四个字段写进应用协议:

  1. 业务流标识 stream。
  2. 单调递增的 sequence 或可排序的事件版本。
  3. 客户端最后成功应用的 lastAppliedSeq。
  4. 回放不可用时的全量同步入口和快照水位。

常见问题

WebSocket 重连后会自动收到断线期间的消息吗?

不会。浏览器会创建新的连接,缺失消息是否可恢复取决于服务端是否保存事件,以及应用协议是否提供回放或同步机制。

只记录客户端收到消息的时间可以吗?

不建议。客户端时间可能漂移,且多个消息可能拥有相同时间戳。使用服务端分配的同一业务流序号更容易判断缺口和去重。

回放接口和实时 WebSocket 必须是两个接口吗?

不必须。它们可以共用一个协议,只要都能表达游标、序号、回放边界和切换水位;分成 HTTP 回放加 WebSocket 实时推送只是更容易拆分实现的一种方案。

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