Post

P2P 技术及其应用

我们知道,常规的视频直播通常是通过 rtsp 或者 hls 协议直播的,这种直播的特点是所有的流量都需要从中心服务器发出,经过 CDN(Content Delivery Network),下发到各个终端。

这种直播适合大量人群观看,但是服务器的带宽成本也很高。

为了节约成本,基于 P2P 技术的音视频直播应运而生,且已广泛应用于在线聊天(webRTC),网络摄像机等领域。

那么什么是 P2P 技术呢?这种技术又如何应用于视频直播呢?

P2P 技术

对等式网络(peer-to-peer, 简称 P2P),又称点对点技术,是无中心服务器、依靠用户群(peers)交换信息的互联网体系,它的作用在于,减低以往网路传输中的节点,以降低资料遗失的风险。与有中心服务器的中央网络系统不同,对等网络的每个用户端既是一个节点,也有服务器的功能,任何一个节点无法直接找到其他节点,必须依靠其户群进行信息交流。

听上去和 Git 有着异曲同工之妙。

所以,只要知道了对方的 IP 地址,就能直接连接了吗?

在没有配置 NPTv6(Network Prefix Translation for IPv6) 的 IPv6 网络下,如果知道了对方的 IPv6 地址,是可以直接连接的,IPv6 地址通常是全球唯一的,因此每个设备可以直接在 Internet 上具有全局可达的 IPv6 地址。

但是,在 IPv4 网络环境下,无法直接连接,因为存在 NAT(Network Address Translation)

NAT 及分类

NAT 技术是为了解决 IPv4 地址短缺问题而提出的。IPv4 地址是 32 位二进制数,可以表示的地址数量是 2 的 32 次方,也就是 4294967296 个。然而,其中一部分地址被保留用于特殊用途,比如私有地址、广播地址、多播地址等,因此实际可用的IPv4地址数量要比这个数字少一些。此外,IPv4 地址的分配也不是均匀的,有些地区和组织分配到的地址比其他地区和组织多,这也导致了 IPv4 地址的短缺问题。

NAT 技术通过将内部网络的私有 IP 地址转换为公共 IP 地址,使得多个内部主机可以共享一个公共 IP 地址访问互联网,从而解决了 IPv4 地址短缺的问题。此外,NAT 技术还可以提高网络安全性,因为它可以隐藏内部网络的 IP 地址,使得外部网络无法直接访问内部网络。

NAT 有三种类型:

  • 静态 NAT,静态一对一的地址转换。需要手动配置映射关系,若网路拓扑改变,则需更新配置,管理与维护难度大。通常用于企业内部网络
内网 IP外网 IP
192.168.1.55219.152.168.222
192.168.1.59219.152.168.223
192.168.1.155219.152.168.224
  • 动态 NAT(Pooled NAT),动态地址池转换。内部网络的私有 IP 地址转换时从一个地址池中选取公有 IP 地址,对应关系有租期限制 内网 IP 和外网 IP 也是一对一的映射关系,和静态 NAT 的区别就是映射关系会动态改变

  • NAPT(Network Address Port Translation,端口地址转换):是一种特殊的动态 NAT,它不仅将内部主机的IP地址映射到一个公共 IP 地址,还将内部主机的端口号映射到公共 IP 地址的不同端口号上,以实现多个内部主机共享一个公共 IP 地址访问外部网络。

内网 IP外网 IP
192.168.1.55:5555219.152.168.222:9200
192.168.1.59:80219.152.168.223:9201
192.168.1.155:4456219.152.168.224:9202

家用路由器一般使用 NAPT,拥有复杂网络的大型公司内网则会使用 Static NATDynamic NAT,本文讨论的 NAT 是第三种 NAT。

锥型和对称型 NAT

第三种 NAPT 又可以分为锥型和对称型 NAT (Symmetric NAT)。锥型 NAT 又可以细分为 Full Cone NAT, Address Restricted Cone NAT, Port Restricted Cone NAT,每一种 NAT 对于外部请求的响应方式有所不同。

  • Full Cone NAT,完全锥型

一旦内部地址(iAddr:iPort)映射到外部地址(eAddr:ePort),所有发自 iAddr:iPort 的数据包都经由 eAddr:ePort 向外发送。任意外部主机都可以通过发送数据包给 eAddr:ePort 给 NAT设备内部的 iAddr:iPort 主机。

  • Address Restricted Cone NAT,地址受限型

在完全锥型 NAT 的基础上限制了 IP 地址。只有和内部地址有过通信的外部地址,才能发送消息

  • Port Restricted Cone NAT,端口受限型

在地址受限型 NAT 的基础上限制了端口,特定外部地址的特定端口,才能发送消息

  • Symmetric NAT,对称型

可以用如下代码在 Chrome 浏览器里检测当前网络的 NAPT 的类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// parseCandidate from https://github.com/fippo/sdp
function parseCandidate(line) {
  var parts;
  // Parse both variants.
  if (line.indexOf('a=candidate:') === 0) {
    parts = line.substring(12).split(' ');
  } else {
    parts = line.substring(10).split(' ');
  }

  var candidate = {
    foundation: parts[0],
    component: parts[1],
    protocol: parts[2].toLowerCase(),
    priority: parseInt(parts[3], 10),
    ip: parts[4],
    port: parseInt(parts[5], 10),
    // skip parts[6] == 'typ'
    type: parts[7]
  };

  for (var i = 8; i < parts.length; i += 2) {
    switch (parts[i]) {
      case 'raddr':
        candidate.relatedAddress = parts[i + 1];
        break;
      case 'rport':
        candidate.relatedPort = parseInt(parts[i + 1], 10);
        break;
      case 'tcptype':
        candidate.tcpType = parts[i + 1];
        break;
      default: // Unknown extensions are silently ignored.
        break;
    }
  }
  return candidate;
};

var candidates = {};
var pc = new RTCPeerConnection({iceServers: [
    {urls: 'stun:stun1.l.google.com:19302'},
    {urls: 'stun:stun2.l.google.com:19302'}
]});
pc.createDataChannel("foo");
pc.onicecandidate = function(e) {
  if (e.candidate && e.candidate.candidate.indexOf('srflx') !== -1) {
    var cand = parseCandidate(e.candidate.candidate);
    if (!candidates[cand.relatedPort]) candidates[cand.relatedPort] = [];
    candidates[cand.relatedPort].push(cand.port);
  } else if (!e.candidate) {
    if (Object.keys(candidates).length === 1) {
      var ports = candidates[Object.keys(candidates)[0]];
      console.log(ports.length === 1 ? 'normal nat' : 'symmetric nat');
    }
  }
};
pc.createOffer()
.then(offer => pc.setLocalDescription(offer))

IP 数据包通过 NAT 设备(如路由器)时,NAT 会重写来源 IP 地址和目的地 IP 地址,从而实现同一内网中不同主机共用公网 IP 地址的功能。

NATNAT示意图

如上图所示,192.168.100.3 的主机向 209.131.36.158 的 80 端口发送 HTTP 请求,经过 NAT 时,NAT 根据其保存的映射表,找到该主机对应的外网 IP 地址和端口,即 145.12.131.7:6282,然后替换 IP 数据包的 Source 字段为该地址,并发送给 www.yahoo.com。雅虎服务器发送 response 的时候,Dest 即为 145.12.131.7:6282,然后 NAT 转发给 192.168.100.33855 端口。

但是,如果某个外网主机想主动访问 192.168.100.3,显然不能到达内网的 192.168.100.3 主机,即使该主机知道了我们的公网 IP,它在主动建立连接时,NAT 发现并没有该地址和局域网内地址的映射,该数据包就会被 NAT 设备丢弃。

NAT 转换表的生成与 TCP/IP 报文的 IP 地址、端口号转换操作会产生一定的开销。

NAT 技术使用非常广泛,但也存在一些缺点:

  1. NAT 设备需要对收发的数据包重新编辑修改(IP 地址转换,重新计算校验),此操作会降低网络数据的传输速度
  2. NAT 设备端口老化问题,会导致连接中的设备异常断开,因为 NAT 设备需要维护端口映射表,而硬件资源有限,所以有些 NAT 设备会定时断开部分连接
  3. 部分网络协议无法通过 NAT 设备,让两台设备直接连接变得困难,因此,才出现了 NAT 打洞技术

NAT 打洞

NAT 打洞是一种技术,用于在两个位于不同私有网络中的设备之间建立直接通信。NAT 打洞通常使用 UDP 协议,所以也称为 UDP 打洞。 下面是 UDP 打洞的基本流程:

  1. 设备 A 和设备 B 都位于不同的私有网络中,它们都无法直接通信。
  2. 设备 A 向设备 B 发送一个 UDP 数据包,这个数据包包含了设备 A 的 IP 地址和端口号。
  3. 设备 B 收到这个数据包后,会记录下设备 A 的 IP 地址和端口号,并向设备A发送一个 UDP 数据包,这个数据包包含了设备 B 的 IP 地址和端口号。
  4. 设备 A 收到这个数据包后,会记录下设备 B 的 IP 地址和端口号。
  5. 现在,设备 A 和设备 B 都知道了对方的 IP 地址和端口号,它们可以直接通过这些信息进行 UDP 通信了。
1
2
3
4
5
6
7
8
9
10
11
12
                            Server S
                        18.181.0.31:5678
                               |
                               |
        +----------------------+----------------------+
        |                                             |
      NAT A                                         NAT B
155.99.25.11:62000                            138.76.29.7:31000
        |                                             |
        |                                             |
     Client A                                      Client B
192.168.0.100:1234                              10.1.1.3:1234

那么问题来了,设备 A 向设备 B 发送一个 UDP 数据包,那么设备 A 如何知道设备 B 的 IP 地址和端口号呢?这就需要中介服务器了。 这个中介服务器可以是一个公网服务器或者是一个 STUN 服务器

TCP 可以打洞吗?UDP 打洞和 TCP 打洞的本质是一样的,都是通过在 NAT 设备上创建映射来实现两个位于不同 NAT 后面的主机之间的直接通信。但是,由于 UDP 协议的特点,UDP 打洞更加简单和高效。

STUN

STUN(Session Traversal Utilities for NAT)是一种用于穿越网络地址转换(NAT)的协议,它通常用于 P2P 通信中的打洞操作。P2P 打洞旨在允许两个设备在 NAT 后进行直接通信,而不需要通过中间服务器。

STUN

Google 提供的 STUN 服务器,可以使用 webRTC 的 demo 测试下。

NAT 有不同的类型,不是所有 NAT 都支持 NAT 打洞,如前面提到的 Symmetric NAT 则无法打洞。UDP 打洞的成功率约为 60%,当打洞失败时,则使用基于 TURN 服务器的中继模式。

TURN

Traversal Using Relays around NAT (TURN)旨在通过打开与 TURN 服务器的连接并通过该服务器中继所有信息来绕过对称 NAT 限制。您将创建与 TURN 服务器的连接,并告诉所有对等方将数据包发送到服务器,然后将其转发给您。这显然会带来一些开销,因此只有在没有其他选择的情况下才使用它。

TURN

经过 TURN 服务器中转的模式,通常称为 relay

参考资料

  • https://en.wikipedia.org/wiki/Network_address_translation
  • https://developer.mozilla.org/en-US/docs/Web/API/WebRTC_API/Protocols
  • https://juejin.cn/post/6844904098572009485
  • https://webrtchacks.com/symmetric-nat/
  • https://www.volcengine.com/docs/6489/72015
This post is licensed under CC BY 4.0 by the author.