WebSocket消息接收顺序错乱非bug,而是协议设计使然;需用client_seq+server_seq双序列号协同排序,重连时先拉取离线消息再重发未确认请求,并通过滑动窗口状态机确保仅confirmed消息触发UI更新。

WebSocket接收顺序错乱不是bug,是协议设计使然
WebSocket 协议只保证单个 TCP 连接内帧(frame)的传输有序,不保证应用层消息(message)逻辑时序。尤其在服务端多线程分发、跨进程负载、弱网重传、客户端切后台再唤醒等场景下,onmessage 触发顺序与业务期望顺序大概率不一致。这不是浏览器或库的缺陷,而是你必须主动接管排序责任——靠序列号,而不是靠“希望它按发送顺序来”。
用 client_seq + server_seq 双序列号锚定真实时序
只用客户端自增 client_seq 会因重连、多端登录、时间回拨而不可靠;只依赖服务端数据库 ID(server_seq)又会导致客户端无法提前做本地缓存排序。必须两者协同:
WebSocket 8.18.2 是该协议规范的一个重要迭代版本,主要优化了连接稳定性与数据传输效率。它通过全双工通信机制,允许客户端与服务器在单一长连接上实时交换数据,大幅降低传统 HTTP 轮询的开销。该版本增强了心跳保活、自动重连及二进制帧传输能力,适用于即时通讯、在线游戏及金融行情推送等低延迟场景,为开发者提供更可靠的实时网络交互基础。
-
client_seq在发送前生成(推荐从localStorage读取并 +1 后存回),随 payload 发出,用于标记“这是第几条我发的请求” - 服务端收到即返回含
client_seq的 ACK,并在落库/处理完成后,推送带server_seq的最终结果(如{"type":"order_filled","client_seq":1005,"server_seq":98765,"data":{}}) - 客户端所有消息流(主动请求响应、离线补推、重连拉取)统一按
server_seq归并进一个有序队列,client_seq仅作关联标识,不参与最终排序
重连后先 fetch_offline 再重发,顺序反了就全乱
重连成功触发 onopen 后立刻重发未确认消息?这是最常见也是最危险的操作。它会让“旧请求的结果”晚于“新请求的结果”到达,UI 状态直接跳跃甚至崩溃。
- 重连第一步:发
{"type":"fetch_offline","last_server_seq":98764},服务端返回server_seq > 98764的所有消息(含已缓存的乱序消息),按server_seq升序排列 - 客户端逐条写入本地有序缓冲区,每条都走完整 ACK 流程,确认后再推进状态机
- 等离线消息全部 confirmed 后,才从待确认队列中取出
client_seq最小的未 ACK 消息重发 - 重发消息仍带原始
client_seq,但服务端需用messageId(非client_seq)查 Redis 去重,避免重复执行
前端滑动窗口 + 状态机,别让 pending 状态污染 UI
很多项目把“收到消息”就当“可展示”,结果用户看到的是 processing 中间态、重复更新、或倒序价格跳变。真正健壮的做法是给每个 client_seq 绑定明确状态:
- 维护一个轻量滑动窗口(例如大小 1024),记录每个
client_seq当前处于:pending→acked→confirmed→timeout - 只对
confirmed状态触发最终 UI 更新;对重复到达的order_report,先查是否已confirmed,是则丢弃 - 提供
getOrderStatus(client_seq)接口,返回确定态,绝不暴露processing这类不可靠中间态 - 服务端没做幂等和快速 ACK,前端状态机再严密也白搭——这点最容易被忽略

















