Apollo 技术文档Apollo 技术文档
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
指南
  • 架构概述
  • BigWorld 架构深度解析
  • BigWorld 进程架构与玩家生命周期
  • AOI九宫格系统详解
  • AOI广播与消息去重
  • Base 模块
  • Core 模块
  • Runtime 模块
  • Data 模块
  • Network 模块
  • /modules/actor.html
  • Game 模块
  • BigWorld 模块
服务器应用
API 参考
QA
GitHub
  • MMORPG 架构 QA

Q22: 如何处理网络消息的乱序问题?

核心结论

乱序问题本质上不是“包到得晚一点”,而是“消息之间存在先后依赖,但网络未按这个依赖顺序交付”。

处理乱序的关键,不是简单要求“所有消息都按序执行”,而是先把消息分类型:

  • 必须严格按序的消息
  • 可以窗口重排的消息
  • 只关心最新状态的消息
  • 即使丢掉旧消息也没关系的消息

如果所有消息都按最严格顺序处理,系统会非常容易被队头阻塞拖慢。

一、为什么会发生乱序

在 TCP 上,应用层通常收到的是有序字节流;但在 UDP、KCP、QUIC 或多通道、多队列、多级转发体系里,乱序非常常见。

即使底层是 TCP,应用层仍然可能出现“逻辑乱序”:

  • 多线程并发处理导致回调顺序变化
  • 不同服务节点返回时间不同
  • 异步 RPC 回包先后不可控
  • 客户端本地缓冲和渲染延迟不同步

所以乱序不只是传输层问题,也是系统设计问题。

二、先区分三类顺序

1. 传输顺序

包在网络上的到达顺序。

2. 处理顺序

消息进入业务逻辑后的执行顺序。

3. 呈现顺序

客户端最终把状态显示给玩家的顺序。

这三者经常不同。很多工程问题正是因为把它们当成一回事。

三、哪些消息必须严格按序

通常包括:

  • 会话建立和销毁
  • 登录、鉴权、切图
  • 背包变更
  • 交易确认
  • 任务状态推进
  • 资产结算

这些消息一旦乱序执行,后果通常是状态错误,而不是单纯体验变差。

处理方式一般是:

  • 强顺序通道
  • 序列号校验
  • 缓冲等待缺失消息
  • 超时后触发重传或失败

四、哪些消息不必强顺序

典型是高频状态消息,例如:

  • 位置
  • 朝向
  • 速度
  • 动画状态

这类消息更重要的是“新鲜”,不是“每一帧都不能丢、不能乱”。

例如位置更新里,pos=102 先到了,pos=101 后到了,后者往往应该直接丢弃,而不是强行补执行。

否则会导致:

  • 角色回弹
  • 画面倒退
  • 插值异常

五、常见处理手段

1. 序列号

这是最基础的一层。

每类有顺序要求的消息都应携带:

  • 会话序列号
  • 流内序列号
  • 或实体级版本号

服务端或客户端根据序列号判断:

  • 是否重复
  • 是否乱序
  • 是否已经过期

2. 重排缓冲区

对允许短时间等待的消息,可以设置一个小窗口缓存:

  • 如果下一条应为 N,但先收到了 N+1
  • 先暂存 N+1
  • 等待 N 到来后再按序处理

这种方案适合:

  • 可靠但可能乱序的消息流
  • 对少量等待可接受的业务

但窗口不能过大,否则延迟会迅速抬高。

3. 只接受最新版本

对状态型消息,经常更适合版本覆盖策略:

  • 只应用版本号更大的状态
  • 旧状态一律丢弃

这特别适合:

  • 位置
  • 朝向
  • 血量快照
  • 实体可见性结果

4. 事件与状态分离

很多系统处理乱序效果差,是因为把“事件”和“状态”混在同一条逻辑链里。

更稳妥的方式是分开:

  • 事件按顺序处理,例如施法开始、伤害结算、死亡确认
  • 状态按版本覆盖,例如位置、动画朝向、当前 HP

这样可以显著降低乱序的复杂度。

六、客户端显示层怎么处理

客户端不是所有消息都要立刻套到画面上。

对远端实体,常见做法是:

  • 维护一个按时间或序列排序的状态缓冲
  • 渲染时读取一个稍早的时间点
  • 在相邻两个合法状态之间插值

这样做的好处是:

  • 能吸收轻微乱序
  • 能降低抖动
  • 不必因为晚到一包就立刻跳变

但前提是这类消息本身适合状态化表达。

七、服务端内部同样要防逻辑乱序

很多人只在客户端处理乱序,但服务端内部问题更隐蔽。

例如:

  • 两个异步 RPC 返回顺序不稳定
  • 持久化完成回调晚于玩家下线
  • 场景切换和战斗结算跨服务返回顺序相反

这类问题靠传输层顺序保证不了,通常要靠:

  • 状态机
  • 版本号
  • 请求上下文 ID
  • compare-and-swap 风格提交

也就是说,系统必须具备“晚到结果自动失效”的能力。

八、工程上更实用的分层策略

一个比较稳妥的做法是:

  • 会话控制类消息:严格按序
  • 资产与任务类消息:严格按序加幂等
  • 高频状态类消息:版本覆盖
  • 客户端显示层:短缓冲加插值
  • 内部异步回调:请求 ID 加状态机校验

这比“一套序列号处理所有消息”更现实。

九、常见误区

1. TCP 就没有乱序问题

不准确。TCP 能保证字节流顺序,但应用层依然可能因为异步处理、并发任务、跨服务返回而出现逻辑乱序。

2. 发现乱序就全部缓存等待

这会把高频实时链路拖慢。很多状态消息应该直接丢弃旧版本,而不是补齐历史。

3. 所有消息都只保留最新

也不对。交易、任务、背包、登录流程都不能这样做,否则会丢掉关键业务步骤。

参考资料

  • Glenn Fiedler, Snapshot Interpolation
  • RFC 9000, QUIC
  • 各类实时游戏网络同步与消息顺序控制实践资料
在 GitHub 上编辑此页
最后更新: 3/20/26, 6:06 AM
贡献者: cuihairu