通过公网STUN服务器实现UDP打洞
通过公网STUN服务器实现UDP打洞
- 一、背景介绍
- 二、原理详解
- 1、NAT的工作原理
- 2、STUN服务器的作用
- 3、UDP打洞的核心思想
- 三、NAT类型对比详解
- 四、测试结果分析
- 五、详细操作步骤
- 1、环境准备
- 2、代码
- 3、完整操作流程
- 3.1、在两台设备上运行脚本
- 3.2、交换公网地址信息
- 3.3、同步开始打洞
- 3.4、验证连接
- 六、实际应用场景
- 七、总结
一、背景介绍
在当今互联网环境中,很多设备都位于NAT(网络地址转换)设备之后,这导致设备之间无法直接建立P2P连接。UDP打洞技术正是为了解决这一问题而诞生的。
什么是UDP打洞?
想象一下,两个人都住在不同的公寓楼里(NAT后面),他们想直接通话而不是通过中间人转接。UDP打洞就像是通过一个公共的"介绍人"(STUN服务器)让双方知道对方的"窗户位置"(公网IP和端口),然后同时向对方的窗户喊话,从而建立直接的联系。
二、原理详解
1、NAT的工作原理
NAT设备就像是大楼的保安,它:
- 将内部设备的私有IP地址转换为公共IP地址
- 记录内部设备发起的连接,并管理返回的数据包
- 通常拒绝外部设备直接发起的连接请求
2、STUN服务器的作用
STUN(Session Traversal Utilities for NAT)服务器是一个公网上的"镜子",它告诉你:
- 你的设备在公网上的IP地址和端口号
- 你的NAT类型和行为特征
3、UDP打洞的核心思想
- 发现阶段:双方都向STUN服务器查询自己的公网地址
- 信息交换:通过服务器交换各自的公网地址信息
- 同时连接:双方几乎同时向对方的公网地址发送UDP包
- 建立通道:NAT设备会认为这些包是之前查询的回复,允许通过
三、NAT类型对比详解
NAT类型 | 开放程度 | P2P打洞难度 | 通俗解释 |
---|---|---|---|
Open Internet | 无NAT | 直接连接 | 住在街边独栋,门牌号公开 |
Full Cone NAT | 完全开放 | 容易 | 公寓保安记住你的脸,任何人都能进来找你 |
Restricted Cone NAT | 中等 | 中等 | 保安只允许你联系过的人进来找你 |
Port Restricted Cone NAT | 较严格 | 较难 | 保安不仅看脸,还要看具体的"敲门方式" |
Symmetric NAT | 最严格 | 最难 | 每次联系不同的人,保安都给不同的门禁卡,无法预测 |
四、测试结果分析
为什么有些组合能成功,有些不能?
-
✅ 二边都是Restricted NAT:可以打通
- 因为双方都先"认识"了对方(通过STUN服务器间接接触)
- NAT设备会允许来自"认识的人"的连接
-
❌ 任意一边是Symmetric NAT:不能打通
- Symmetric NAT每次对外连接都用不同的端口
- 对方无法预测你会用哪个端口来接收数据
- 就像每次用不同的电话号码打电话,对方无法回拨
五、详细操作步骤
1、环境准备
首先确保你的Python环境已就绪:
# 检查Python版本
python3 --version# 安装必要的库
pip3 install pystun3
2、代码
cat > hole_punching_utils.py << 'EOF'
import threading
import socket
import random
import select
import time
import socket
import binascii
import stun
import socket
import struct
import randomdef is_udp_port_free(host='localhost', port=0):"""检查UDP端口是否空闲"""try:with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s:s.bind((host, port))return Trueexcept OSError:return Falsedef get_reliable_free_udp_port(host='localhost', min_port=54320, max_port=65535):"""可靠地获取空闲UDP端口"""max_attempts = 50for attempt in range(max_attempts):port = random.randint(min_port, max_port)if is_udp_port_free(host, port):# 双重检查if is_udp_port_free(host, port):return port# 如果随机选择失败,让系统分配return get_free_udp_port()def _receiver_loop(local_socket):"""接收循环 - 处理所有传入数据"""while True:try:ready = select.select([local_socket], [], [], 1.0)if ready[0]:data, addr = local_socket.recvfrom(65536)print(addr,data.decode('utf-8'))breakexcept BlockingIOError:continueexcept OSError as e:print(f"接收数据时发生OS错误: {e}")breakexcept Exception as e:print(f"接收数据时发生未知错误: {e}")print("接收循环结束")def main():# 1. 准备通信工具(创建UDP socket)source_ip="0.0.0.0"source_port=get_reliable_free_udp_port(source_ip)# 2. 准备多个STUN服务器(备用镜子)stun_hosts=['stun.miwifi.com','stun.easyvoip.com','stun.voipbuster.com']stun_port=3478 # STUN标准端口# 3. 创建并配置UDP sockets = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)s.settimeout(2)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)s.bind((source_ip, source_port))# 4. 查询NAT类型 - 了解自己的"公寓保安"类型find=Falsefor stun_host in stun_hosts:nat_type, nat = stun.get_nat_type(s, source_ip, source_port,stun_host=stun_host, stun_port=stun_port)print(stun_host,nat_type,nat)if nat_type in ['Restric NAT']:break# 5. 获取公网地址信息 - 了解自己的"窗户位置"external_ip = nat['ExternalIP']external_port = nat['ExternalPort']# 6. 启动接收线程 - 准备听对方喊话thread = threading.Thread(target=_receiver_loop, args=(s,))thread.daemon = Truethread.start()# 7. 显示并交换地址信息print(f"你的公网地址: {external_ip}:{external_port}")print("请将这个地址告诉对方,同时获取对方的公网地址")# 8. 获取对方地址try:input_str = input("请输入对方公网IP:端口 (格式: 192.168.1.1:12345): ").strip().split(":")target_ip = input_str[0]target_port = int(input_str[1]) target = (target_ip, target_port)print(f"🎯 目标地址: {target}")except:print("❌ 输入格式错误,请按 IP:端口 格式输入")return# 9. 开始打洞 - 双方同时"向对方窗户喊话"input("按回车键开始打洞(确保对方也准备好了)...")hostname = socket.gethostname()seq_num=0print("🚀 开始发送数据包...")try:while True:message = f"{hostname}:消息#{seq_num}"s.sendto(message.encode('utf-8'), target)print(f"📤 发送: {message}")time.sleep(1) # 每秒发送一条消息seq_num += 1except KeyboardInterrupt:print("\n⏹️ 用户停止程序")
if __name__ == "__main__":main()
EOF
python3 hole_punching_utils.py
3、完整操作流程
3.1、在两台设备上运行脚本
# 在设备A上运行
python3 hole_punching_utils.py# 在设备B上运行(同时或稍后)
python3 hole_punching_utils.py
3.2、交换公网地址信息
设备A显示:
🎯 你的公网地址: 123.45.67.89:54321
请将这个地址告诉对方,同时获取对方的公网地址
设备B显示:
🎯 你的公网地址: 987.65.43.21:12345
请将这个地址告诉对方,同时获取对方的公网地址
交换方式:
- 设备A输入设备B的地址:
987.65.43.21:12345
- 设备B输入设备A的地址:
123.45.67.89:54321
3.3、同步开始打洞
关键要点:
- 双方都要在准备好后按回车
- 时间间隔尽量短,最好在10秒内完成
- 如果第一次失败,可以多试几次
3.4、验证连接
成功迹象:
📤 发送: DeviceA:消息#0
📤 发送: DeviceA:消息#1
('987.65.43.21', 12345) DeviceB:消息#0 # 收到对方消息!
📤 发送: DeviceA:消息#2
六、实际应用场景
UDP打洞技术被广泛应用于:
- 视频会议系统:如Zoom、Teams的P2P模式
- 在线游戏:玩家间的直接连接
- 文件共享:BitTorrent等P2P下载
- 物联网设备:设备间的直接通信
七、总结
UDP打洞就像是通过一个公共介绍人让两个躲在门后的人建立直接联系。虽然技术原理复杂,但通过STUN服务器的帮助,我们能够巧妙地利用NAT设备的行为特性实现直接通信。
记住成功的关键因素:
- ✅ 合适的NAT类型(非对称NAT)
- ✅ 准确的公网地址信息
- ✅ 双方几乎同时发起连接
- ✅ 稳定的网络环境