udp打洞不能直接用sendto就通,因为nat设备仅允许已发起出站连接的外部ip:端口回包,需双方同步向对方公网映射地址发送udp包以建立双向映射通道。

UDP打洞为什么不能直接用sendto就通?
因为大多数家用路由器默认开启NAT(网络地址转换),客户端发出的UDP包会被映射到一个临时的公网端口,而这个映射关系有超时机制,且不会主动向内转发未经请求的包。单纯调用sendto发包过去,对方收不到——不是你没发出去,是对方的NAT设备根本没开“门”。打洞的本质,是让双方在NAT上各自建立一条可被对方识别的映射通道,并在同一时间向对方映射地址发包,触发双向“开门”。
如何用getsockname和STUN获取自己在NAT后的公网映射地址
打洞前必须知道自己的“外网身份”,即NAT分配的IP:port。本地bind用的是0.0.0.0:0,系统随机选端口,但这个端口对公网不可见。你需要通过STUN服务器(如stun.l.google.com:19302)查询实际映射结果:
- 创建UDP socket,
connect到STUN服务器(非必需但简化后续逻辑) - 构造STUN Binding Request(最简可用
0x0001消息类型 + 20字节头部),sendto过去 - 解析响应中的
XOR-MAPPED-ADDRESS或MAPPED-ADDRESS属性,提取公网IP和port - 注意:某些NAT(如对称型)会为不同目标IP分配不同端口,所以务必用同一个STUN服务器、同一socket做探测
打洞时sendto的时机和目标地址怎么填?
双方拿到彼此的公网映射地址后,不能立刻发业务数据。必须先“敲门”:各自用自己已bind的socket,向对方的公网IP:port调用sendto(哪怕只发1字节空包)。关键点在于:
- 必须双方几乎同时发(误差控制在1–3秒内),否则先发的一方映射可能超时失效
-
sendto的目标地址必须是对方刚从STUN拿到的mapped address,不是对方内网地址,也不是你本地getsockname的结果 - 第一次
sendto失败(errno == ECONNREFUSED或静默丢包)是正常的,只要对方NAT记录了这次请求,后续你的包就能进来了 - 建议用独立线程/定时器控制发送节奏,避免依赖用户交互导致时间差过大
打洞成功后,recvfrom为什么还是收不到?
打洞只是打通NAT通道,不等于连接建立。常见掉坑点:
- socket没设
SO_REUSEADDR,重启程序时端口被TIME_WAIT占用,导致新进程bind失败,实际用的不是预期端口 - 防火墙(尤其是Windows Defender或macOS防火墙)拦截了UDP入站,需手动放行你的可执行文件
- 对方发来的第一个包,你还没来得及
recvfrom就结束了程序——务必在sendto之后保持socket open并循环recvfrom至少5秒 - 部分运营商级NAT(CGNAT)根本不支持UDP打洞,STUN返回的IP可能是运营商内网地址,这种情况下打洞必然失败
真正能稳定工作的场景,基本限于双端都在家庭宽带(非CGNAT)、NAT类型为全锥型或受限锥型。对称型NAT或企业防火墙环境下,大概率需要中继(TURN)兜底。
C++免费学习笔记(深入):立即使用
在学习笔记中,你将探索 C++ 的入门与实战技巧!











