欢迎光临
我们一直在努力

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\\n(本地IP)}
    ICE –> D{Server Reflexive Candidate\\n(STUN获取)}
    ICE –> E{Relay Candidate\\n(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。”

    毕竟,没有你们,我们都“连不上”。📞✨

    赞(0)
    未经允许不得转载:171主机测评 » STUN/TURN服务器穿透NAT建立P2P连接
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址