UDP不可靠,WriteToUDP成功仅表示数据进入内核队列,不代表对方收到;丢包无提示,必须应用层实现序列号、ACK和time.Timer重传机制。

UDP 本身不报错、不重传、不保序,WriteToUDP 返回 nil 只代表数据进了内核发送队列,不代表对方收到。丢包检测和重传必须由应用层自己做,没有捷径。
为什么不能依赖 WriteToUDP 的返回值判断送达
常见错误是看到 WriteToUDP 没报错,就认为包发成功了。实际上:sendto: no route to host 或 write: broken pipe 是系统级错误,但绝大多数丢包(比如中间路由器静默丢弃、接收端缓冲区溢出)根本不会触发任何 Go 层错误。发送端完全无感知,直到上层业务超时失败。
-
WriteToUDP成功只表示“内核收下了”,不是“对方收到了” - UDP 没有连接状态,
net.Conn接口对*net.UDPConn不适用——它不提供SetReadDeadline之外的流控能力 - 别在
for循环里反复WriteToUDP而不等 ACK:这等于在制造拥塞,丢包率会指数上升
序列号 + ACK + time.Timer 是最小可行组合
停等协议(Stop-and-Wait)是最易验证的起点,适合调试和小流量场景。关键不是“多快”,而是“不错”。
- 每个包头部预留 4 字节
seq(uint32),用binary.BigEndian.PutUint32编码,避免结构体内存布局陷阱 - 接收端收到后立即回一个极简 ACK 包,只含
ack_seq(同样uint32),不带 payload - 发送端为每个发出的 seq 启动一个专属
*time.Timer,超时触发重传;收到对应ack_seq后立刻调timer.Stop() - 绝对不要复用同一个
time.Timer实例管理多个 seq——Reset()在已触发状态下返回false,漏判会导致重传失控
并发读写滑动窗口时最容易 panic 的三个点
Go 的 net.UDPConn 是线程安全的,但你自定义的 sendBuf、recvMap、inflight 映射不是。竞态往往出现在边界条件。
立即学习“go语言免费学习笔记(深入)”;
- 接收协程正在更新
rcv_nxt,发送协程同时清理inflight映射:用独立sync.Mutex锁inflight,别锁整个结构体 - 多个重传定时器 goroutine 同时访问同一份
inflight状态:每次timer触发前先mu.Lock(),检查该 seq 是否已被 ACK,确认未确认后再重发,然后mu.Unlock() - 接收端缓存乱序包用
map[uint32][]byte,但没加锁就直接delete(recvMap, seq):读多写少场景下,用sync.RWMutex保护该 map,交付连续段后批量清理
超时时间不能写死,初始值只是起点
固定 100ms 或 500ms 的重传间隔在真实网络中基本无效。弱网下要么重传太慢,业务卡死;要么重传太勤,加剧丢包。
- 初始 RTO 建议设为 200ms,RTTvar 初始 50ms,RTO = rtt + 4 × rttvar
- 每次收到 ACK,用 RFC6298 的公式更新 rtt 和 rttvar:
rttvar = 0.75*rttvar + 0.25*abs(sample_rtt - rtt),rtt = 0.875*rtt + 0.125*sample_rtt - 重传次数上限设为 3–5 次,再失败应主动关闭会话并通知上层——无限重试只会让问题更难定位
真正难的不是实现重传逻辑,而是让所有状态(seq、ack、timer、buffer)在高并发、弱网、频繁断连下保持一致。哪怕只漏掉一次 timer.Stop() 或一次 mu.Unlock(),都会导致后续包永远卡在重传队列里。动手前先想清楚谁读、谁写、谁删、谁等。


















