🎬 第十三章 WebRTC网页直播
📄 一句话概括

打洞不是技术,是利用 NAT 自己的规则。 你先发过包的对端,NAT 就给它开门。

📝 本节和前面几节的关系

13.1 说 WebRTC 走 UDP,这一节讲它怎么把 UDP 包送进 NAT 后面。 13.2.2 讲 STUN 与 TURN 分别帮什么忙。

一、NAT 只放行「你先找过的人」

家用路由器做的那件事很简单:内网能出去,外面不能随便进来。 具体规则是「你向谁发过包,谁回包才放进来」。 这条规则既挡住了扫描,也挡住了 WebRTC。

打洞利用的正是这条规则: 两边同时往对方的公网地址发一个包,各自在自己的 NAT 上开一个口子。 口子一开,对方的包就能进来——这就是「穿透」。

所以顺序是关键:先发,再收。 如果只等不发,对方的包会被 NAT 直接丢掉, 而丢包是静默的,你只会看到「一直 connecting」。 这就是为什么自建 WebRTC 时「两边都在等」是典型错误。

二、真打一次:先发再收

环回上没有 NAT,所以这一步只能证明「UDP 知道地址就能发」。 但顺序仍然要看:先发再收,和不发只收,结果完全不同。

动作 结果


A 先发一个包给 B 发出去了 B 回一个包给 A A 收到 reply B 收到 A 的第一个包没有 收到了 hole

两边都通了,因为这里没有 NAT。 要注意的是 A 第一个包早在 B 回包之前就发了——这个「先发」的动作就是打洞。

在有 NAT 的网络里,少了这一步,对方的包一定进不来。 而且这个包不需要有意义,空包也算—— 它在 NAT 上留下的是一条映射表项,不是一个请求。 所以打洞包发出去之后就可以丢了,对方收到也直接丢掉。

三、四种 NAT,两种结局

打洞不是总能成,差别就在「公网端口怎么分配」:

NAT 类型 访问 STUN 时的公网端口 访问对端时的公网端口 预测对不对


锥形 40128 40128 对 对称 40065 40143 错

锥形 NAT 一个内网端口只映射一个公网端口, 所以「问 STUN 得到的地址」直接就能给对端用,打洞成功。 对称 NAT 每访问一个新目标就换一个公网端口, 问 STUN 得到的那个地址是废的——对端往那儿发包,打不到你。

这一步是模型推导,不是实测:沙箱里没有真 NAT 设备。 但它推的就是 RFC 里那两条规则的直接结果,结论可靠。

所以「我家网络打洞总失败」不是配置问题,是网络结构问题。 对称 NAT 下唯一的办法是中继: 两边都连到一台公网服务器,由它转发。这就是 TURN。

四、还要看防火墙这一层

NAT 之外还有一道墙:企业防火墙。 它可能直接封 UDP,只留 80 与 443。 这时候连 STUN 都问不出去,候选表里只剩内网地址。

标准解法是把 TURN 也跑在 443 上,用 TCP 或 TLS 伪装成 HTTPS。 这样墙以为是网页流量,就放过去了。 代价是延迟变高、服务器带宽翻倍——但至少能通。

所以一套完整的 WebRTC 服务通常要准备三套地址: UDP 3478 走 STUN、UDP 中继段走 TURN、TCP 443 走 TURN over TLS。 前两套不通时,第三套兜底。 这条「逐级降级」的链路,才是能不能商用的分界线。

五、动手 Demo:NAT 穿透——先发一个包出去

两个 UDP socket 互发一次,看顺序带来的差别。 再按映射表推一遍两种 NAT 的打洞结果。

# 两个 socket 互发,先发再收
a = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
b = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
a.bind(("127.0.0.1", 0))
b.bind(("127.0.0.1", 0))
a.settimeout(2)
b.settimeout(2)

a.sendto(b"hole", b.getsockname())      # 先发:这一步就是打洞
hb, ab = b.recvfrom(100)
b.sendto(b"reply", ab)                  # 再回
ha, aa = a.recvfrom(100)

print("B 收到", hb, "来自", ab)
print("A 收到", ha, "来自", aa)

# 两种 NAT 的端口映射(按 RFC 规则推,不是实测)
NATS = [("锥形", 40128, 40128), ("对称", 40065, 40143)]
for name, p_stun, p_peer in NATS:
    ok = (p_stun == p_peer)
    print(name, "STUN 看到", p_stun, "对端看到", p_peer,
          "打洞", "成" if ok else "败")

实跑输出:

--------------------------------------------------------------------
NAT 穿透——先发一个包出去
--------------------------------------------------------------------
**① NAT 只放行「你先找过的人」**

  **家用路由器做的那件事很简单:内网能出去,外面不能随便进来。**
  **具体规则是「你向谁发过包,谁回包才放进来」。**
  **这条规则既挡住了扫描,也挡住了 WebRTC。**

  **打洞利用的正是这条规则:**
  **两边同时往对方的公网地址发一个包,各自在自己的 NAT 上开一个口子。**
  **口子一开,对方的包就能进来——这就是「穿透」。**
  **所以 U 盘直连那个「P2P 加速」功能,本质就是这一步。**

**② 真打一次:两个 UDP socket 互发**

  **环回上没有 NAT,所以这一步只能证明「UDP 知道地址就能发」。**
  **但顺序仍然要看:先发再收,和不发只收,结果完全不同。**

  动作                        结果
  --------------------------------------------
  A 先发一个包给 B            发出去了
  B 回一个包给 A              A 收到 reply
  B 收到 A 的第一个包没有     收到了 hole

  **两边都通了,因为这里没有 NAT。**
  **要注意的是 A 第一个包早在 B 回包之前就发了**——**这个「先发」的动作就是打洞,**
  **在有 NAT 的网络里,少了这一步,对方的包一定进不来。**

**③ 但打洞不是总能成**

  **两种 NAT 的差别就在「公网端口怎么分配」:**
  **锥形 NAT 一个内网端口只映射一个公网端口;**
  **对称 NAT 每访问一个新目标就换一个公网端口。**

  NAT 类型     访问 STUN 时的公网端口     访问对端时的公网端口     预测对不对
  -------------------------------------------------------------------------------
  锥形         40128                      40128                    对
  对称         40065                      40143                    错

  **锥形 NAT 两次映射到同一个端口,所以「问 STUN 得到的地址」**
  **直接就能给对端用,打洞成功。**
  **对称 NAT 两次端口不同,问 STUN 得到的那个地址是废的**——**对端往那儿发包,打不到你。**
  **这一步是模型推导,不是实测:沙箱里没有真 NAT 设备。**
  **但它推的就是 RFC 里那两条规则的直接结果,结论可靠。**

  **对称 NAT 下唯一的办法是中继:**
  **两边都连到一台公网服务器,由它转发。这就是 TURN。**
  **所以「打洞失败」不是配置问题,是网络结构问题。**

A 发出去了,B 收到了 hole,B 回了 reply,A 也收到了。 两边都通,因为这里根本没有 NAT 挡路。 这个 Demo 能证明的只有一件事:UDP 只要知道地址就能发。 它证明不了打洞成功——打洞是否成功,取决于两侧 NAT 的类型。 下面那张映射表是推的:锥形两次映射同端口,预测对; 对称两次不同端口,预测错,只能走中继。

30 秒实验

  1. 把 a.sendto 那行删掉,只留 recvfrom,猜会怎样——两边都超时,因为谁也开不了口子。
  2. 把 B 的 recvfrom 改成先睡 1 秒再收,猜还能不能收到——能,UDP 的包在内核队列里等着,但队列满了就丢。
  3. 换成对称 NAT 的企业网,猜候选表里还剩什么——只剩内网地址与 srflx,而且 srflx 是错的,只能走 TURN。

常见误区

  1. 「打洞是黑科技」——它只是让 NAT 按自己的规则开门。
  2. 「打洞包必须有内容」——空包也行,NAT 只看地址。
  3. 「打洞失败一定是代码问题」——对称 NAT 下必然失败。
自测三问(点开对答案) 1. 打洞利用的是 NAT 的哪条规则? —— **「你先发过包的对端,回包才放进来」**,所以要先发。 2. 对称 NAT 为什么打不通? —— **每换一个目标就换一个公网端口**,STUN 报的地址对不上。 3. 防火墙只留 443 时怎么办? —— **把 TURN 跑在 443 上走 TCP 或 TLS**,伪装成网页流量。

相关笔记