STUN/TURN服务器穿透NAT建立P2P连接
STUN/TURN服务器穿透NAT建立P2P连接
你有没有遇到过这种情况:两个手机明明连着同一个Wi-Fi,视频通话却卡得像幻灯片?或者打游戏时对面说“你人呢”,其实你早就进了房间——但就是连不上。🤯
问题很可能出在 NAT(网络地址转换) 上。
如今几乎每台设备都躲在路由器后面,共享一个公网IP。这虽然缓解了IPv4枯竭的问题,但也让“点对点直连”变成了一道难题: 我怎么知道你在哪?你又怎么进得来我的内网?
这时候,STUN 和 TURN 就登场了——它们不是魔法,但干的几乎是魔法的事:帮你从层层防火墙和NAT中“钻”出一条通路,哪怕最后只剩一条窄窄的中继通道,也要确保通信不断 💪
咱们今天不整那些“首先…其次…”的AI八股文,直接上硬货。来聊聊这两个协议是怎么协作打通P2P连接的,顺便看看为什么WebRTC能实现“开箱即用”的音视频通话。
想象一下你要寄信给朋友,但你住在一个没有门牌号的小区里。邮递员只知道你从哪个大门出去的。这时候你怎么办?
STUN 干的就是这个事: 问一句:“我是从哪扇门出来的?”
它的工作原理特别简单:
- 客户端往公网上的 STUN 服务器发个 UDP 包。
- 服务器一看:“哦,你是从
1.2.3.4:5678进来的。” - 回复:“兄弟,你现在对外的地址是这个。”
于是客户端就知道了自己的“公网身份”——也就是所谓的 Server Reflexive Address (服务器反向映射地址)。
这一步完成后,它就可以告诉对方:“嘿,想跟我通信?往 1.2.3.4:5678 发数据就行!”
当然,能不能收到,还得看 NAT 类型脸色。
常见的 NAT 类型有四种:
| NAT类型 | 是否允许外部主动连接 |
|---|---|
| Full Cone | ✅ 只要你知道出口地址,随便发 |
| Restricted Cone | ✅ 同一IP可发,不同IP不行 |
| Port-Restricted Cone | ✅ 必须是你之前发过包的IP+端口 |
| Symmetric NAT | ❌ 每次目标不同,出口端口都变,极难穿透 |
如果是前三种,STUN 基本就能搞定;但如果两边都是 Symmetric NAT?那不好意思, STUN 失效 😵💫
这时候就得靠 TURN 出场了。
如果说 STUN 是个“地址查询员”,那 TURN 就是个“快递中转站”。
当 P2P 直连失败时,TURN 会为你分配一个公网上的中继地址(Relay Address),所有数据都通过它转发:
Client A ←→ TURN Server ←→ Client B
哪怕你们都在深不见底的企业防火墙后面,也能通!
整个流程分三步走:
-
Allocation
客户端向 TURN 申请一个中继地址:“给我留个收件箱!”
服务器返回一个公网 IP:port,比如203.0.113.1:10000 -
Create Permission
“我想让B往我这发东西。”
服务器把这个 IP 加入白名单,防止垃圾流量泛滥。 -
Data Relay
- B 发送到 Relay Address → TURN 收到并转发给 A
- A 发送数据 → TURN 代为转发给 B
整个过程就像是租了个公共邮箱,别人把信投进去,管理员再亲手交给你 📦
而且为了效率,TURN 还支持 Channel Binding :把常用的对端地址绑定成短 ID(如 0x0001),以后用 4 字节 Channel Data 替代完整的 UDP/IP 头部,省带宽又提速。
别看这些协议名字冷冰冰,它们可是现代实时通信的“幕后英雄”。
以 WebRTC 为例,一次音视频通话的背后,其实是 ICE + STUN + TURN 的黄金三角在撑场子:
graph TD
A[Client A] -->|收集candidates| ICE
B[Client B] -->|收集candidates| ICE
ICE --> C{Host Candidate
(本地IP)}
ICE --> D{Server Reflexive Candidate
(STUN获取)}
ICE --> E{Relay Candidate
(TURN分配)}
A <-->|交换SDP| Signaling(Server)
B <-->|交换SDP| Signaling
Signaling --> A
Signaling --> B
A -->|连通性检查| F[尝试host→host]
A -->|失败则试| G[尝试srflx→srflx]
A -->|仍失败| H[最终走relay→relay]
H --> I[媒体流建立成功]
整个流程行云流水:
- 双方各自跑一遍 STUN/TURN,生成一堆候选地址(candidates)
- 通过信令服务器交换 SDP 描述符(里面包含了所有 candidate)
- ICE 引擎开始“挨个试”:先试试局域网直连(host),不行就试试公网映射地址(srflx),最后实在没办法才走中继(relay)
优先级通常是:
host > srflx > relay
也就是说, 能直连绝不中转,万不得已才走 TURN ——这样既保证了成功率,又尽可能降低了延迟。
来点实战代码感受下 STUN 的真实操作 👇
下面这段 Python 脚本模拟了一个最基础的 STUN 请求,连接 Google 的公共 STUN 服务:
import socket
import struct
import random
# STUN Message Types
STUN_BINDING_REQUEST = 0x0001
STUN_MAGIC_COOKIE = 0x2112A442
def create_stun_message():
transaction_id = bytes([random.randint(0, 255) for _ in range(16)])
message = struct.pack('!HHI16s', STUN_BINDING_REQUEST, 0, STUN_MAGIC_COOKIE, transaction_id)
return message
def parse_stun_response(data):
if len(data) < 20:
return None
_, _, cookie, transaction_id = struct.unpack('!HHI16s', data[:20])
pos = 20
while pos + 4 <= len(data):
attr_type, attr_len = struct.unpack('!HH', data[pos:pos+4])
if attr_type == 0x0020: # XOR-MAPPED-ADDRESS
port = struct.unpack('!H', data[pos+6:pos+8])[0] ^ (STUN_MAGIC_COOKIE >> 16)
ip = '.'.join(str(b ^ (STUN_MAGIC_COOKIE >> (8*(3-i))) & 0xff) for i, b in enumerate(data[pos+8:pos+12]))
return f"{ip}:{port}"
pos += 4 + ((attr_len + 3) // 4) * 4
return None
# 发送STUN请求
sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
stun_server = ("stun.l.google.com", 19302)
message = create_stun_message()
try:
sock.sendto(message, stun_server)
sock.settimeout(5)
response, _ = sock.recvfrom(1024)
public_addr = parse_stun_response(response)
print(f"Public IP:Port detected by STUN: {public_addr}")
except Exception as e:
print(f"STUN request failed: {e}")
finally:
sock.close()
运行结果可能是这样的:
Public IP:Port detected by STUN: 1.2.3.4:5678
看到了吗?这就是你的设备在公网上的“出口”。浏览器里的 WebRTC 模块初始化时,做的第一件事就是类似的操作。
⚠️ 提醒一句:生产环境千万别手搓协议!推荐使用成熟库,比如
aiortc或pystun3,否则容易踩加密、重传、超时等各种坑。
那如果真要用 TURN,该怎么搭个可用的服务呢?
开源界有个神器叫 coturn ,部署起来也不复杂。
配置文件 /etc/turnserver.conf 示例:
listening-port=3478
tls-listening-port=5349
external-ip=YOUR_PUBLIC_IP
realm=turn.example.com
user=username:password
fingerprint
lt-cred-mech
total-quota=100
bps-capacity=1000000 # 每用户限速1Mbps
启动命令:
turnserver -c /etc/turnserver.conf
客户端连接信息如下:
URI: turn:turn.example.com:3478?transport=udp
Username: username
Credential: password
就这么几行配置,你就拥有了一个可靠的中继节点。移动端切换网络、企业内网穿越、Symmetric NAT 场景统统拿下 ✅
不过要注意: TURN 是按流量计费的资源大户 。如果你的应用日活百万,全靠中继转发音视频流,服务器带宽成本可能直接爆表 💸
所以最佳实践永远是: STUN 主力冲锋,TURN 压阵兜底 。
实际开发中,还有几个关键点必须注意:
🛡️ 安全防护不能少
- 开启
stale-nonce防止重放攻击 - 使用临时令牌(token-based auth)替代静态密码
- 限制每个用户的并发 allocation 数量,防滥用
🌐 高可用设计要提前考虑
- 多地域部署 TURN 节点(AWS、GCP、阿里云各来一台)
- 配置 DNS SRV 记录实现自动故障转移
- 结合负载均衡器做健康检查
📊 成本与性能平衡
- 设置合理的带宽配额(bps-capacity)
- 日志监控异常流量(比如突然暴涨的 relay 数据)
- 在调试阶段可用
iceTransportPolicy: "relay"强制走中继验证逻辑
说到这里,你应该明白了:为什么 Zoom、Teams、微信视频通话能在各种复杂网络环境下依然稳定工作。
背后正是这套 ICE + STUN + TURN 的组合拳在默默发力。
它们不一定最快,但足够聪明;不一定最省,但足够可靠。
未来随着 IPv6 普及,NAT 逐渐退出历史舞台,也许有一天我们不再需要这些“穿墙术”。但在那之前,STUN 和 TURN 依然是构建实时通信系统的“最后一公里”守护神 🛠️
所以啊,下次你顺利打进一个视频会议时,不妨心里默念一声:
“谢谢你,STUN;辛苦了,TURN。”
毕竟,没有你们,我们都“连不上”。📞✨








