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

Q13: 什么是可靠 UDP?如何实现?

核心结论

“可靠 UDP”不是一个固定协议名,而是一类思路:

  • 底层仍然用 UDP 发送数据报
  • 但在应用层自己补上部分可靠传输能力

它的价值不在于“完全替代 TCP”,而在于:

  • 只为真正需要的消息补可靠性
  • 保留 UDP 更轻、更灵活的传输语义
  • 避免把所有消息都塞进 TCP 的统一可靠字节流里

所以真正关键的问题不是“能不能做可靠 UDP”,而是:

  • 为什么要自己补可靠
  • 要补到什么程度
  • 哪些消息值得可靠
  • 哪些语义不能照搬 TCP

一、为什么会需要可靠 UDP

如果只看两种极端选择:

  • TCP:可靠、有序,但语义比较重
  • UDP:轻量、灵活,但默认不可靠

那么很多实时系统会落在中间地带:

  • 有些消息不能丢
  • 但又不想让所有消息都被 TCP 的队头阻塞和统一重传策略拖住

典型例子:

  • 技能释放请求不应该轻易丢
  • 重要战斗事件最好可靠到达
  • 但高频位置状态不值得逐条补发

这时就会出现一个思路:

在 UDP 上自己实现“选择性可靠”。


二、可靠 UDP 解决的不是“全都可靠”,而是“按需可靠”

这是最容易被说错的地方。

可靠 UDP 的目标通常不是复刻一个完整 TCP,而是:

  • 对关键消息补 ACK 和重传
  • 对旧状态消息继续允许丢弃
  • 对顺序要求高的流做独立排序
  • 对不同消息类型给出不同传输语义

也就是说,可靠 UDP 的价值往往来自“消息分级”。

如果你把所有消息都放进同一个可靠 UDP 通道,最终很可能只是重新发明了一个更难维护的 TCP。


三、一个可靠 UDP 最基础要补哪些机制

3.1 序列号

要知道包的先后顺序,首先需要序列号。

它的作用包括:

  • 判断是否丢包
  • 判断是否乱序
  • 判断是否重复

没有序列号,就谈不上可靠性控制。

3.2 确认机制

发送方必须知道哪些包已经到达。

常见方式:

  • ACK:确认收到
  • NACK:明确说明某些包没收到

ACK 是可靠 UDP 的核心反馈通道。

3.3 重传机制

关键消息没到,就要决定是否重发。

但这里要非常注意:

  • 不是所有消息都值得重传
  • 超过时效的消息重传没有意义

所以可靠 UDP 的重传通常是“有条件重传”,不是“无脑重传”。

3.4 去重

一旦有重传,就一定可能重复到达。

所以接收方必须能识别:

  • 这个包是第一次来
  • 还是之前已经处理过

否则同一条技能、同一条交易事件可能被执行多次。

3.5 乱序处理

UDP 不保证顺序。

所以如果某些消息流要求有序,就要自己决定:

  • 是缓存等待缺失包
  • 还是跳过旧包
  • 还是直接按最新状态覆盖

这一步不能统一处理,必须按消息类型决定。


四、可靠 UDP 最关键的不是“补机制”,而是“别补过头”

很多自研可靠 UDP 最大的问题不是功能不够,而是补得太像 TCP。

4.1 典型过度设计

  • 所有包都强制可靠
  • 所有包都强制有序
  • 所有丢包都必须等重传
  • 所有流共享一个阻塞窗口

这样做的后果通常是:

  • 失去 UDP 的灵活性
  • 重现 TCP 的阻塞问题
  • 实现复杂度更高
  • 结果却不一定更好

4.2 更合理的做法

更成熟的可靠 UDP 通常会拆成多种消息语义:

  • 可靠事件流
  • 非可靠状态流
  • 可丢旧保新的状态快照流

这样才能真正发挥它的价值。


五、一个更实用的实现思路

5.1 把消息先分成三类

第一类:可靠事件

例如:

  • 技能释放
  • 伤害事件
  • 关键状态切换
  • 战斗中的重要控制事件

特点:

  • 不能随便丢
  • 重传有意义
  • 通常需要幂等处理

第二类:非可靠高频状态

例如:

  • 位置
  • 朝向
  • 高频移动状态

特点:

  • 旧消息不重要
  • 不值得重传
  • 只要最新状态尽快到达

第三类:半可靠状态

例如:

  • 中频状态快照
  • 需要大体一致,但旧包价值会快速下降的状态

这类消息可以:

  • 做有限次重传
  • 超时后直接放弃

5.2 按消息类型选不同处理策略

真正成熟的实现通常不是“一种可靠 UDP 规则处理所有消息”,而是:

  • 可靠事件通道:ACK + 重传 + 去重
  • 高频状态通道:无重传,按最新覆盖
  • 半可靠通道:有限重传,超时丢弃

六、可靠 UDP 与 TCP 最大的语义差异

6.1 TCP 的默认语义

TCP 默认帮你做的是:

  • 整条字节流可靠
  • 整条字节流有序
  • 后面的内容通常要等前面的内容先补齐

6.2 可靠 UDP 更适合的语义

可靠 UDP 更常见的目标是:

  • 某些消息可靠
  • 某些消息不可靠
  • 某些消息乱序可接受
  • 某些消息过期就作废

这就是两者最大的差别。

所以可靠 UDP 的优势不是“比 TCP 更可靠”,而是:

它允许你把不同消息的可靠性语义拆开。


七、实现时最容易忽略的几个点

7.1 幂等

只要有重传,就要考虑重复执行。

所以关键事件必须设计成:

  • 要么天然幂等
  • 要么带事件 ID 做去重

否则“可靠”只会把错误放大。

7.2 超时消息是否还值得重传

有些消息 50ms 内有意义,200ms 后就没意义了。

例如:

  • 过时的位置状态
  • 过时的动作表现

这类消息即使丢了,也不应该补。

7.3 发送窗口和拥塞

如果你只补了 ACK 和重传,却没有任何限速和窗口控制,在差网络下只会:

  • 越丢越重发
  • 越重发越拥塞
  • 整体效果更差

所以哪怕不做完整 TCP 拥塞控制,也至少要有:

  • 基本窗口
  • 基本节流
  • 基本超时退避

7.4 观测能力

自研可靠 UDP 如果没有可观测性,几乎不可维护。

至少要能看到:

  • RTT
  • 丢包率
  • 重传率
  • 重复包率
  • 乱序率
  • 窗口占用

不然线上出问题时只会看到“玩家说卡”,但不知道卡在哪层。


八、什么时候值得做可靠 UDP

8.1 值得做的情况

  • 实时交互明显受 TCP 语义限制
  • 已明确需要区分可靠事件和非可靠状态
  • 团队有能力长期维护传输层逻辑
  • 项目确实需要弱网下更好的战斗体验

8.2 不值得急着做的情况

  • 还没有先把消息分层
  • 当前瓶颈其实在业务线程、AOI、广播或序列化
  • 团队没有网络协议维护经验
  • 项目主体业务其实以可靠状态管理为主

这种情况下,盲目上可靠 UDP 很容易把问题从一个地方搬到另一个地方。


九、和 KCP 的关系

KCP 可以看成是一种很成熟的“可靠 UDP 思路实现”。

它已经帮你补了很多基础能力:

  • 序列号
  • ACK
  • 重传
  • 窗口
  • RTT 估算

所以很多项目真正的选择不是:

  • “要不要可靠 UDP”

而是:

  • “自己造一套,还是直接用 KCP 这类成熟方案”

多数情况下,如果只是需要一套可用的可靠 UDP 机制,优先复用成熟实现会更稳。


十、总结

可靠 UDP 的本质,不是“把 UDP 改造成 TCP”,而是:

  • 保留 UDP 的灵活性
  • 只为需要的消息补可靠性
  • 避免让所有消息共享同一种重传和阻塞语义

更稳妥的工程实践通常是:

  • 先把消息分层
  • 再决定哪些消息需要可靠 UDP
  • 最后再决定是自研还是直接使用成熟实现

如果消息分层都还没做清楚,就直接讨论可靠 UDP 的实现细节,通常会把系统做得过重。


参考资料

  • KCP
  • KBEngine GitHub - reliable_udp
  • Selective Acknowledgment
在 GitHub 上编辑此页
最后更新: 3/20/26, 6:06 AM
贡献者: cuihairu