UDP打洞必须先让双方都发包给对方,因为NAT(尤其对称型)为每个目标IP:端口单独建映射,仅A→服务器的映射无法支持A→B通信;只有双方互发包后NAT才记录对应外部端点,后续数据才能穿透。

UDP打洞为什么必须先让双方都发过包给对方
因为NAT设备(尤其是对称型NAT)会为每个目标IP:端口组合单独建立映射表项。如果A只向服务器发包,NAT只记下“A→服务器”的出口映射;B同理。此时A直接发包给B的公网IP:端口,NAT发现没这条映射,直接丢弃——根本不会转发出去。只有A和B都主动向对方的公网地址发过至少一个UDP包,NAT才可能在映射表里“记住”这个外部端点,后续包才能穿透。
如何用sendto()触发NAT映射并维持保活
关键不是“打通一次”,而是让映射持续有效。大多数家用路由器NAT映射超时在30–120秒之间,不发包就会失效:
- 双方需在获取到对方公网
IP和port后,立即用sendto()各发1–3个空包(或带简单标识的包)到对方地址,触发映射建立 - 之后每20–25秒必须重发一次保活包(内容可为
"keepalive"或单字节0x00),防止映射老化 - 注意:保活包必须用和首次打洞相同的本地
socket、相同源端口,否则NAT会新建映射,旧映射仍会超时 - 不要依赖
connect()——它会限制socket只能收发指定地址,P2P中需接收任意来源的响应包
为什么recvfrom()要设为非阻塞且配合select()或poll()
打洞阶段双方都在“盲发”,但谁先收到对方的包谁就获得主动权。如果用阻塞recvfrom(),可能卡死在等待上,错过打洞窗口:
- 设置socket为非阻塞:
fcntl(sock, F_SETFL, O_NONBLOCK)(Linux)或ioctlsocket(sock, FIONBIO, &nonblocking)(Windows) - 用
select()监听socket可读事件,超时设为100–500ms,避免空转耗CPU -
recvfrom()返回-1且errno == EAGAIN(或WSAEWOULDBLOCK)是正常情况,继续轮询即可 - 一旦收到非本机服务器发来的包(即源IP:port ≠ 服务器地址),立刻标记“打洞成功”,切换为常规通信逻辑
常见失败原因和调试手段
打洞失败80%以上源于NAT类型或防火墙配置,而非代码逻辑:
立即学习“C++免费学习笔记(深入)”;
- 对称型NAT(如部分企业网络、4G/5G移动热点)基本无法P2P直连,
STUN返回的mapped address每次请求都不一样,打洞必然失败 - 防火墙丢弃“非请求响应”的UDP包:确认两端系统防火墙(Windows Defender / iptables)允许UDP入站,且未启用“仅允许已建立连接”策略
- 客户端拿到的公网IP是CGNAT地址(如
100.64.x.x):说明运营商做了二层NAT,此时必须走中继,UDP打洞无效 - 调试建议:用
tcpdump -i any udp port <your_port>或Wireshark抓包,确认“自己发出的包是否真到了对方公网IP”,以及“对方发来的包是否到达本机网卡”——这是判断问题在NAT侧还是代码侧的分水岭
真正难的从来不是写几行sendto和recvfrom,而是确认对方NAT是否允许你“被洞穿”。打洞只是把钥匙递过去,门开不开,得看门锁本身支不支持这种钥匙。


















