Yet another blog of Matthew Lee 👀
Full-Stack Developer, good at Android 🤖️

P2P 打洞原理

November 08, 2020

上篇文章 中,我们知道了 WebRTC 本身是点对点(Peer-to-Peer)连接的。如果连接双方都是公网地址,则可以直接访问到对方,从而建立连接。但大部分情况下其中一方或者双方都不是公网地址,而是隐藏在 NAT(Network Address Translation,网络地址转换)之后的内网地址,此时要建立连接,就得使用某种能绕过 NAT 的打洞技术。

NAT

在开始介绍打洞技术之前,我们先了解一下什么是 NAT。NAT 全称 Network Address Translation,即网络地址转换,可以理解为公网地址和内网地址的映射规则。NAT 最早是作为 iPv4 地址短缺的一种解决方案而流行起来的。

以常见的基于地址和端口转换的 NAPT 为例。假设有台路由器的公网地址为 145.12.137.7;同时内网有三台设备连接到了这台路由器上,其中一台内网地址为 192.168.100.3 的设备想要访问公网地址为 209.131.36.158 的 yahoo.com,则会发生如下过程:

图片引用自维基百科「Network address translation」条目
图片引用自维基百科「Network address translation」条目

  1. 192.168.100.3 这台机器上端口为 3855 的程序(可能是浏览器)发出 IP 数据包,源地址为 192.168.100.3:3855,目标地址为 209.131.36.158:80(80 端口为 HTTP 协议的默认端口)。
  2. IP 数据包经过路由器时,路由器对源地址进行查表;如果源地址不在表中,路由器会自动分配一个端口号对应它,图中对应的端口号就是 6282;同时其内网 IP 也会被替换为公网 IP。
  3. IP 数据包经过路由器后,源地址变为 145.12.137.7:6282,而目标地址不变。

于是 192.168.100.3 这台机器便可以访问到 yahoo.com 了。同理,当 yahoo.com 的数据包返回时,路由器会进行反向查表,将请求结果转发给 192.168.100.3。

打洞

假设存在两台设备 A 和 B,它们分别位于各自的 NAT_A 和 NAT_B 之后。此时 A 第一次尝试和 B 建立点对点连接,向 NAT_B 发送数据包;然而 NAT_B 经过查表发现,之前并没有 A 和 B 的映射(即 A 的请求无法被转发到 B),于是来自 A 的数据包就会被丢弃。

为了能绕过 NAT 的这一限制,我们需要借助一台公网上的服务器 S 做地址转发。流程如下:

图片引用自 https://bford.info/pub/net/p2pnat
图片引用自 https://bford.info/pub/net/p2pnat

  1. A 与 S 建立连接(Session A-S),向 S 注册自己的内网地址 10.0.0.1:4321;S 会同时记录 A 在公网的地址 155.99.25.11:62000。B 与 S 建立连接(Session B-S),向 S 注册自己的内网地址 10.1.1.3:4321;S 会同时记录 B 在公网的地址 138.76.29.7:31000。
  2. A 向 S 发送请求,获取 B 的地址(Request Connection to B);S 会同时把 A 的地址转发给 B(Forward A’s Endpoints to B)。然后 A 和 B 都开始尝试相互向对方发送数据包。
  3. 当 A 向 B 第一次发送数据包时(Send to B at),会在 NAT_A 中产生映射 (10.0.0.1:4321, 138.76.29.7:31000);此时 NAT_B 并没有 A 和 B 的映射记录,于是数据包仍然会被丢弃。
  4. 当 B 向 A 第一次发送数据包时(Send to A at),会在 NAT_B 中产生映射 (10.1.1.3:4321, 155.99.25.11:62000);因为之前 NAT_A 已经创建了 A 和 B 的映射,所以 B 请求成功。
  5. 当 A 向 B 第二次发送数据包时,因为 NAT_B 也有了 A 和 B 的映射记录,所以 A 也请求成功。于是打洞完成,A 和 B 可以直接建立点对点连接(Session A-B)。

有的读者可能会产生疑问:为什么要向 S 上报内网地址?假设存在两台设备 A1 和 A2,它们同时位于 NAT_A 之后,且它们并不知晓各自的地址。当 A1 和 A2 通过上述步骤建立连接后,它们会发现双方都在同一内网,而所有数据包仍然需要通过 NAT_A 转发是没有必要的,直接使用内网地址建立连接显然更快。

真实的网络情况可能会更加复杂,比如需要在多层 NAT 之间打洞。以及目前业界习惯使用 UDP 协议进行打洞,而不是 TCP 协议。若读者想要了解更多打洞细节,可以参考这篇文章:Peer-to-Peer Communication Across Network Address Translators