UDP可靠传输本质是构建状态机闭环,需自主管理序号、ACK、超时、滑动窗口及乱序缓存,不能仅依赖简单重传;sendto/recvfrom不维护状态,所有字段须网络字节序,动态RTO优于固定超时。

不能靠加个重传就完事——UDP可靠传输的本质是状态机闭环,不是单点补丁。
UDP可靠传输必须自己管序号、ACK和超时,别指望内核
Winsock的 sendto 和 recvfrom 只负责发/收数据报,不维护任何连接状态。你发出去的每个包必须带自定义 SeqNum(比如 uint16_t 或 uint32_t),接收方回的每个 ACK 也得带 AckNum 字段;否则根本不知道哪一包丢了、该重哪一包。没有序号,重传就是盲猜;没有 ACK,你就永远卡在“发了但不知对方收没收到”的悬停态。
- 序号类型选
uint32_t更安全,避免uint16_t在高速场景下快速回绕导致误判 -
AckNum应表示“已成功接收的最大连续序号 + 1”,不是简单回传收到的SeqNum - 所有协议字段(如
SeqNum、AckNum)必须用htonl/htons转网络字节序,否则跨平台收发会错乱
滑动窗口不是可选项,而是防止ACK泛洪和乱序堆积的关键
停等协议(stop-and-wait)每发一包就等 ACK,吞吐极低,RTT 50ms 时理论极限才 20 包/秒。真实场景要用滑动窗口控制并发未确认包数。比如设窗口大小为 16,你就可以连续发 16 个包再等 ACK,只要收到任一 AckNum,就滑动窗口左边界向前推进。
- 发送端需维护
unackedPackets映射表(std::map),存待确认包及发送时间戳 - 接收端要支持乱序缓存:收到
SeqNum=5但还没收到 3、4,就得暂存 5,等齐后再按序交付上层 - 窗口大小别硬写死,建议从 8~32 开始调,局域网可放大,公网高丢包环境反而要收小
动态RTO比固定超时靠谱得多,Karn算法必须配合RTTVAR
用固定 1s 超时,在局域网里太保守(实际 RTT 常 RTO = SRTT + 4 * RTTVAR,其中 SRTT 是平滑 RTT,RTTVAR 是偏差估计值;每次收到 ACK 都更新,且 Karn 算法规定:超时重传的包,其 RTT 不参与下次计算(因为无法判断 ACK 对应的是初传还是重传)。
- 初始化
SRTT = 0.5s、RTTVAR = 0.25s是较稳妥起点 - 定时器线程每 10ms 检查一次
unackedPackets,对超时项触发重传并指数退避(如首次重传后 RTO × 1.5) - 别用
SetTimer这类 Windows GUI 定时器——它精度差、易丢失,改用CreateWaitableTimer或std::thread+std::this_thread::sleep_until
校验和、去重、心跳一个都不能少,否则上线就崩
UDP 层本身有 16 位校验和,但只检伪头+UDP头+数据,且 Winsock 默认可能关闭校验(SO_NO_CHECKSUM 之类),应用层必须自己加。另外,IP 层重复投递、网络设备重传都可能导致同包多次到达,仅靠序号不够,还得用 receivedSet(如 std::unordered_set)缓存最近收到的序号,5 秒内重复直接丢弃。
- 校验和推荐 CRC32(非 Fletcher),速度快、碰撞率低,别手写简单异或
- 心跳包必须独立于数据流:空 payload + 特殊 flag,且服务端收到后必须回心跳 ACK,否则单向断连无法感知
- 所有缓冲区(
SendBuffer、RecvBuffer)必须预分配、内存池化,避免高频 new/delete 触发锁争用或碎片
最常被忽略的是状态迁移完整性——比如 SYN 包丢了,客户端不断重发,服务端却因没收到而永远不建连接;或者 FIN-ACK 失配导致连接卡在 CLOSE_WAIT。这些必须在状态机里显式定义超时兜底(如 SYN_SENT 状态 3s 无响应则重试或失败),不能依赖“理论上对方总会回”。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











