UDP 与游戏协议设计
UDP & Game Protocol Design
动作游戏的时间关键数据(输入、位姿、物理)只关心最新状态。TCP 的可靠有序流会在丢包时把头阻塞住;正确做法是游戏协议只走 UDP,自己做虚拟连接、按需可靠性与拥塞退避,并按数据类型选择「冗余重发」还是「直接丢弃旧包」。
TCP 为何不适合实时玩法数据
TCP 在 IP 之上伪装成文件流:可靠、有序、带流控。丢失一包就要停下来等重传(常见约 RTT~2×RTT,拥塞退避时更糟),后到的新数据也得排队。FPS 里玩家输入与角色位置过期即废,停等只会冻住画面与操作,随后一坨积压包同一帧涌入。(UDP vs. TCP)
TCP_NODELAY 只能关掉 Nagle 对小包的合并,治不好队头阻塞。
不要把游戏协议拆成 UDP+TCP
TCP 与 UDP 共享 IP 路径,TCP 的拥塞行为可能加剧 UDP 丢包;两边各做一套 NAT/可靠性也更难控。旁路用 HTTP 拉 REST 可以,但玩法协议应完整留在 UDP 上。(UDP vs. TCP)
虚拟连接
在无连接的 UDP 上约定:双方以固定频率互发带 协议 ID 的包即视为在线;服务端可把「第一份合法协议包」当作连上,用 recvfrom 地址过滤;用「多久没收到包」做超时断线。(Virtual Connection over UDP)
序列号、Ack 与 Bitfield
目标不是复刻 TCP,而是:稳定发包,并知道对端收到了哪些包。
包头典型字段:protocol id、sequence、ack、ack bitfield(32 bit)。本地每发一包递增 sequence;对端用 ack + bitfield 冗余确认最近约 33 个序号。从不对同一 sequence 重传;若应用层数据必须再送,装进新序号的新包。约 1 秒未获 ack 可推断丢失(概率判断)。16 位序号要处理回绕比较。(Reliability and Congestion Avoidance over UDP)
sequenceDiagram participant A as 端 A participant B as 端 B A->>B: seq=10 数据 A->>B: seq=11 数据 Note over B: 丢了 seq=10 B->>A: seq=5 ack=11 bitfield含缺口 Note over A: 知 11 已到、10 可能丢;新数据用 seq=12 发送
上图:丢包不冻结后续包;由应用决定是否用新包补数据。
按数据类型选可靠性
| 数据类型 | 策略 |
|---|---|
| 角色快照 / 最新位姿 | 不可靠;丢了跳到更新的快照(见 快照插值) |
| 很小的帧输入(锁步) | 每个 UDP 包冗余携带所有未确认输入,直到 ack(见 确定性锁步同步) |
| 可靠事件 / 大块 | 应用层消息 ID + 按需重发;可拆块(Gaffer 后续文) |
小输入冗余发送能在「需要可靠有序」的场景下仍击败 TCP,因为不必为单包重传卡住整条流。(Deterministic Lockstep)
简单二进制拥塞控制
测 RTT:发送队列记 (seq, 时间),ack 时取样本,对当前 RTT 做指数平滑(约 10%)。RTT 超阈值(如 250 ms)立刻降到「坏模式」发包率(如 10 pps);持续良好一段时间再回到「好模式」(如 30 pps)。短时间反复跌落则拉长恢复等待,避免震荡。(Reliability and Congestion Avoidance over UDP)目的是避免路由器排队把延迟顶到数秒。
与引擎层的关系
UE 复制与 RPC 建立在引擎网络驱动之上,开发者很少手写 UDP;但「不可靠组播 RPC vs 可靠」「属性复制丢旧取新」的语义,与本页同一套原则一致。见 网络复制与 RPC。