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 的实现细节,通常会把系统做得过重。
