UDP本身不保证可靠传输和顺序,Go的net.UDPConn仅封装系统socket,无法解决丢包乱序;需在应用层实现序列号、ACK、重传等机制,KCP(如kcp-go)可辅助但需手动管理连接、窗口、超时等。

UDP丢包和乱序在Go里没法靠net.Conn自己解决
UDP协议本身不保证送达、不保证顺序,Go标准库的net.UDPConn只是对系统UDP socket的封装,它不会帮你重传、排序或校验。你看到数据没到、或者后发的包先到了,不是代码写错了,是UDP本来就这样。
常见错误现象:ReadFromUDP返回成功但业务逻辑发现消息缺失;时间戳相近的包顺序颠倒;高频发送时某几条永远收不到。
- 别试图用“多读几次”或“加sleep”来缓解——这只会掩盖问题,且在高并发下更不可靠
- 如果业务要求可靠有序(比如实时指令、状态同步),必须在应用层加协议,不能依赖UDP底层
- 简单心跳+超时重发能应付低频场景,但无法处理网络抖动下的重复、乱序,需要序列号+ACK机制
KCP在Go中不是开箱即用的“UDP升级版”
kcp-go是KCP协议最常用的Go实现,但它不是替换了net.UDPConn就能自动变可靠——你得主动管理连接生命周期、收发缓冲、流控和错误回调。
使用场景:弱网环境下的实时音视频控制信令、游戏状态同步、IoT设备远程指令下发。
立即学习“go语言免费学习笔记(深入)”;
-
kcp-go默认启用nodelay和fastresend,适合低延迟场景,但会增加带宽占用;生产环境建议关掉fastresend或设为1–2,避免重传风暴 - 每个
kcp.UDPSession要配独立的net.UDPConn,不能多个session共用一个底层conn,否则读写冲突 - 务必设置
SetWindowSize(如(32, 32)),否则窗口太小会导致吞吐卡死;但设太大又吃内存,需按单连接预期吞吐调优 - 收到
io.EOF不代表连接断开,KCP的Close才是真释放资源;没显式Close会导致goroutine泄漏
乱序包在KCP里由接收窗口自动重排,但业务层仍要防“过期数据”
KCP内部用滑动窗口+序列号做排序,应用层调用Read拿到的数据一定是按发送顺序的。但这不等于“最新”——网络延迟波动大时,旧包可能晚到几秒才被提交。
典型问题:玩家A发出“跳跃”指令,100ms后又发“停止”,结果“跳跃”包延迟了300ms才到,服务端按顺序执行,导致动作错乱。
- 在业务解包前,检查消息里的逻辑时间戳或序列号,直接丢弃明显滞后的包(比如比当前窗口最大序列小50以上)
- 不要依赖KCP的
RecvDelay统计值做业务判断——它是平滑估算,不准;自己记录每条消息的接收时间戳更可靠 - KCP的
SetNoDelay参数影响的是内部探测包频率,不是业务消息延迟,别误以为开了就“绝对低延迟”
丢包重传不是免费的,kcp-go的CPU和内存开销容易被低估
KCP为了低延迟做了大量定时器和缓冲区操作,每个连接默认维护至少4个goroutine(recv、send、update、ack),加上滑动窗口的slice分配,在万级连接时GC压力明显。
性能影响点:频繁Write小包(1KB)又可能被IP层分片,增加丢包概率。
- 批量写入优于逐条
Write:把多条业务消息打包成一个[]byte再发,减少KCP内部锁竞争和timer触发次数 - 用
SetMtu匹配实际网络路径MTU(通常1400–1450),别用默认1400硬扛千兆内网——内网可设到8900,降低分片率 - 监控
kcp.Stat里的Lost和Rtt,当Rtt > 300ms且Lost > 5%持续10秒,该切备用通道或降级协议了
真正麻烦的不是怎么接住包,而是怎么判断哪个包该信、哪个该扔,以及扔了之后要不要补、怎么补——这些边界逻辑,KCP不替你做,Go也不会提醒你漏写了。










