STUN 只回答「我从外面看是什么地址」,一通一答就完。 TURN 是「你说不通我替你转」,成本翻倍。
13.2.1 讲了打洞的原理,这一节讲谁来告诉你对端地址。 13.3.1 把这两个服务真搭起来。
一、两个东西,两件事
STUN 只回答一个问题:「你从外面看,是什么地址?」 它不转发任何媒体,一通一答就结束,开销极小。 TURN 是「你说不通,那我替你转」——它要真搬数据,成本高得多。
对比项 STUN TURN
干的事 告诉你公网地址 替你转发媒体 带宽成本 几乎没有 双向翻倍 默认端口 3478 3478 什么时候用 总是先问 打洞失败才用
两个都用 3478,是因为它们本来就是同一个协议族。 TURN 是在 STUN 的基础上加了一组「分配与转发」的消息类型。
所以部署时经常是同一个进程同时干两件事, Coturn 就是这样:3478 上既答 STUN 也接 TURN。 你不需要装两个软件。
二、真收发一次绑定请求
STUN 报文很简单:20 字节头,后面跟属性。
头里有三样:消息类型、长度、魔术字 0x2112A442,再加 12 字节事务号。
这里手工组一个请求,服务端回一个带 XOR-MAPPED-ADDRESS 的响应。 两个字段要盯: ① 事务号必须原样回来,对不上说明回的不是这一次的答案; ② 映射地址必须和客户端自己绑定的一致(这里没有 NAT)。
请求 20 字节、响应 32 字节,往返几百微秒。 这就是 STUN 的全部开销——它小到可以在每次建连前都问一遍。
在真的 NAT 后面,这个值就是 NAT 分配的公网地址, 而它只能通过这种「出去再问回来」的方式得到。 本机自己查网卡是查不到的——网卡上只有内网地址。
三、XOR 那一层是防谁
地址为什么不直接写,要和魔术字异或一下? 因为早期的中间设备会「聪明地」改写报文里的 IP 地址。 那种设备叫 ALG,本意是帮 VoIP 修地址,实际经常改错。 而异或之后报文里没有可识别的 IP 二进制,它就改不动了。 这是典型的「为了让设备变笨,主动把格式弄乱」。
端口也一样:端口 ^ (魔术字 >> 16)。
所以解析时必须原样再异或回来,
直接按普通字节读会得到一个完全不相干的数字。
这条经验可以推广:凡是和旧设备打过交道的协议, 都会留几处「故意让人看不懂」的设计。 看到这类怪格式,先别急着说设计得烂, 去查一下它的历史,八成是被某类中间设备坑过。
四、TURN 的成本为什么是两倍
中继一路流,服务器要收一份、再发一份。 所以同一个观众占用的出口带宽是直连时的两倍。
方式 服务器出口 成本
直连(P2P) 0 不占服务器带宽 TURN 中继 400 Mbps 入 200 Mbps 出 200 Mbps
100 个人看 2048 kbps 的流,走中继就要 400 Mbps 出口, 而直连是 0。 这就是为什么所有 WebRTC 服务都拼命想让打洞成功。
total-quota 这一条就是干这个的:限制同时中继的路数。
它不是「限制用户数」,是「限制转发通道数」,两者不是一回事。
一个用户可能开好几个通道(多个标签页),也可能一个都不开(直连成功)。
所以看监控要看「当前分配数」,不是「在线人数」。
五、动手 Demo:STUN 与 TURN——手工收发一次绑定请求
手工组一个 STUN 绑定请求,起一个只会回映射地址的服务端。 顺便把 TURN 的中继成本按人头算一遍。
MAGIC = 0x2112A442
def stun_req(txid):
"""20 字节头:类型 0x0001、长度 0、魔术字、12 字节事务号。"""
return struct.pack("!HHI", 0x0001, 0, MAGIC) + txid
def stun_resp(txid, ip, port):
"""回一个带 XOR-MAPPED-ADDRESS 的响应(属性类型 0x0020)。"""
xport = port ^ (MAGIC >> 16)
xip = bytes(a ^ b for a, b in zip(socket.inet_aton(ip),
struct.pack("!I", MAGIC)))
val = struct.pack("!BBH", 0, 0x01, xport) + xip
attr = struct.pack("!HH", 0x0020, len(val)) + val
return struct.pack("!HHI", 0x0101, len(attr), MAGIC) + txid + attr
txid = os.urandom(12)
req = stun_req(txid)
resp = stun_resp(txid, "127.0.0.1", 49422)
print("请求", len(req), "字节,响应", len(resp), "字节")
back = resp[20:32]
print("事务号对得上", back == txid)
val = resp[24:]
xport = struct.unpack("!H", val[2:4])[0] ^ (MAGIC >> 16)
xip = socket.inet_ntoa(bytes(a ^ b for a, b in
zip(val[4:8], struct.pack("!I", MAGIC))))
print("映射地址", "%s:%d" % (xip, xport))
实跑输出:
--------------------------------------------------------------------
STUN 与 TURN——手工收发一次绑定请求
--------------------------------------------------------------------
**① 两个东西,两件事**
**STUN 只回答一个问题:「你从外面看,是什么地址?」**
**它不转发任何媒体,一通一答就结束,开销极小。**
**TURN 是「你说不通,那我替你转」**——**它要真搬数据,成本高得多。**
对比项 STUN TURN
--------------------------------------------------
干的事 告诉你公网地址 替你转发媒体
带宽成本 几乎没有 双向翻倍
默认端口 3478 3478
什么时候用 总是先问 打洞失败才用
**两个都用 3478,是因为它们本来就是同一个协议族。**
**TURN 是在 STUN 的基础上加了一组「分配与转发」的消息类型。**
**② 真收发一次 STUN 绑定请求**
**STUN 报文很简单:20 字节头,后面跟属性。**
**头里有三样:消息类型、长度、魔术字 `0x2112A442`,再加 12 字节事务号。**
**这里手工组一个请求,服务端回一个带 XOR-MAPPED-ADDRESS 的响应。**
项目 值
------------------------------------------
请求字节 20
响应字节 32
消息类型 0x0101
事务号对得上 是
映射地址 127.0.0.1:62456
它等于客户端的地址 是
往返耗时 659 微秒
**请求 20 字节、响应 32 字节,往返几百微秒。**
**这就是 STUN 的全部开销——它小到可以在每次建连前都问一遍。**
**解析出来的地址是 `127.0.0.1:62456`,正是客户端自己绑定的那个。**
**因为这里没有 NAT,所以「从外面看」和「自己看到的」一样。**
**在真的 NAT 后面,这个值就是 NAT 分配的公网地址,**
**而它只能通过这种「出去再问回来」的方式得到。**
**③ XOR 那一层是干什么的**
**地址为什么不直接写,要和魔术字异或一下?**
**因为早期的中间设备会「聪明地」改写报文里的 IP 地址。**
**而异或之后报文里没有可识别的 IP 二进制,就改不动了。**
**这是典型的「为了让设备变笨,主动把格式弄乱」。**
**端口也一样:`端口 ^ (魔术字 >> 16)`。**
**所以解析时必须原样再异或回来,**
**直接按普通字节读会得到一个完全不相干的数字。**
请求 20 字节、响应 32 字节,一个包一问一答就结束。
事务号原样回来,说明这条响应确实是给这一次请求的。
映射地址解析出来是 127.0.0.1:49422,正是客户端绑定的那个。
因为这里没有 NAT,「外面看到的」和「自己看到的」一样。
真的 NAT 后面,这个值就是公网地址,网卡上查不到。
注意解 XOR 的顺序:先和魔术字异或回来,再解释成地址。
跳过这一步,读出来的会是一个看起来像地址、其实毫无意义的数字。
30 秒实验
- 把事务号故意改掉一位再解析,猜会怎样——地址照样解得出来,但按协议必须丢弃,因为它不是这次的答案。
- 把 XOR 那一步去掉,直接按普通字节读,猜得到什么——一个完全不相干的地址,比如 33.18.180.5 这类。
- 把
total-quota从 100 改成 10,猜谁会受影响——只有走中继的第 11 个人开始被拒,直连的人一点感觉都没有。
常见误区
- 「STUN 会转发媒体」——它只报地址,不搬数据。
- 「TURN 和 STUN 是两个软件」——Coturn 一个进程干两件事。
- 「total-quota 是限制在线人数」——它限制的是中继通道数。
自测三问(点开对答案)
1. STUN 报文头部固定多少字节? —— **20 字节**:类型 2、长度 2、魔术字 4、事务号 12。 2. XOR-MAPPED-ADDRESS 为什么要异或? —— **防止 NAT 设备里的 ALG 自作聪明改地址**。 3. 为什么 TURN 的带宽是两倍? —— **服务器要收一份再发一份**,进出各算一次。- ☐ (本节完,下一节 13.3.1 基于 Coturn 项目的 STUN/TURN 服务器搭建)