新手区 记一次 P2P 通信的实践

心向东 · 2018年01月16日 · 最后由 Starnny 回复于 2020年04月08日 · 2812 次阅读

最近突然对 P2P 的实现感了兴趣,所以在查阅了一些资料后,自己实现了最简单的 P2P 功能。正好许久没来社区码字,在这里记录和分享一下过程中的知识点。
因为主要作为记录用,所以文中提到的相关知识点只会讲述个大概不会大篇幅展开,肯定会有所纰漏,想了解详细信息的同学可以自行查阅相关资料。

说到 P2P 呢大家应该都听说过, 生活中其实很多应用都用到了这项技术,比如某些即时通信软件、某些需要实现多人联机的游戏都或多或少的会使用这项技术。
点对点通信最简单的实现就是两台计算机在知道对方 IP 地址的情况下,直接连接对方端口并发送数据。如果在局域网中这一条件非常容易达成,但在互联网中因为众所周知的 ipv4 地址短缺而 ipv6 迟迟不能普及等问题,每台计算机根本无法直接分配到一个外网 IP,没有明确的 IP 地址,两台计算机就无法直接进行点对点通信。

关于ipv4和ipv6其实也是个很有意思的话题,但是我这边篇幅有限就不详细展开了,想了解的可以查阅网上其他的资料

想要了解如何在互联网中实现 P2P,必须知道很多相关的知识点,下面我一点点讲解。

“公网” 和 “内网” 地址

  • 内网 IP 地址: 是指使用 A/B/C 类中的私有地址, 分配的 IP 地址在全球不惧有唯一性,也因此无法被其它外网主机直接访问。
  • 公网 IP 地址: 是指具有全球唯一的 IP 地址,能够直接被其它主机访问的。

举例 : 你在家使用电信、网通或者其他宽带服务时,你的个人 PC 所分配到的 IP 地址叫内网 IP 地址(一般为 192.168.x.x)。当你访问一个网站时,网站服务器看到你的来源 IP 并不是你内网 IP 地址,而是一个公网 IP 地址(由网络服务器商提供)。

那为什么会有公网 IP内网 IP呢, 那就要引出下一个知识点了。

NAPT(Network Address Port Translation),网络端口地址转换

NAPT是NAT的进阶技术运用,这里暂时不展开NAT相关的资料了
随着网络的普及,IPv4 的局限性暴露出来。公网 IP 地址成为一种稀缺的资源,NAPT 实现了多台私有 IP 地址的计算机可以同时通过一个公网 IP 地址来访问 Internet 的功能。这在很大程度上暂时缓解了 IPv4 地址资源的紧张。
NAPT 负责将某些内网 IP 地址的计算机向外部网络发出的 TCP/UDP 数据包的源 IP 地址转换为 NAPT 自己的公网的 IP 地址,源端口转为 NAPT 自己的一个端口。目的 IP 地址和端口不变, 并将 IP 数据包发给路由器,最终到达外部的计算机。同时负责将外部的计算机返回的 IP 数据包的目的 IP 地址转换内网的 IP 地址,目的端口转为内网计算机的端口,源 IP 地址和源端口不变,并最终送达到内网中的计算机。

上面的例子是单次通信的例子,其实 NAPT 的处理逻辑远没有如图上画的那么简单。
NAPT 还分很多种类型,有些类型并不适合实现 P2P 通信,所以接下来就要降到 NAT 的类型知识点了。

NAT 的四种类型

  1. Full Cone NAT(全锥 NAT): 全锥 NAT 把所有来自相同内部 IP 地址和端口的请求映射到相同的外部 IP 地址和端口。任何一个外部主机均可通过该映射发送数据包到该内部主机

  2. Restricted Cone NAT(限制性锥 NAT): 限制性锥 NAT 把所有来自相同内部 IP 地址和端口的请求映射到相同的外部 IP 地址和端口。但是, 和全锥 NAT 不同的是:只有当内部主机先给外部主机发送数据包, 该外部主机才能向该内部主机发送数据包

  3. Port Restricted Cone NAT(端口限制性锥 NAT): 端口限制性锥 NAT 与限制性锥 NAT 类似, 只是多了端口号的限制, 即只有内部主机先向外部地址:端口号对发送数据包, 该外部主机才能使用特定的端口号向内部主机发送数据包。

  4. Symmetric NAT(对称 NAT): 对称 NAT 与上述 3 种类型都不同, 不管是全锥 NAT ,限制性锥 NAT 还是端口限制性锥 NAT ,它们都属于锥 NAT(Cone NAT )。当同一内部主机使用相同的端口与不同地址的外部主机进行通信时, 对称 NAT 会重新建立一个 Session ,为这个 Session 分配不同的端口号,或许还会改变 IP 地址。

对于实现 P2P 通信来说,各种 NAT 类型的实现难度依次为 对称型 > 端口受限锥型 > 受限锥型 > 全锥型,所以我们在实现 P2P 通信前还需要测试我们所处的 NAT 环境属于哪种类型。

NAT 类型测试

前提条件: 有两个公网的 IP 地址(IP-1,IP-2),并在开启两个 UDP 端口监听 (IP-1,Port-1),(IP-2,Port-2)。

顺便提一下,做这个测试的时候需要两台带外网 IP 的服务器,我个人只有 1 台测试用的外网服务器,得想办法再搞一台。我从阿里云那申请了一台时间为一周的突发性实例,只用了 15 块,如果大家想自己测试的话 完全可以花 30 快租个两台机器进行测试。

现在我有两台服务器,并开启两个服务 (IP1:Port1 )(IP2:Port2 )

第一步:检测客户端是否有能力进行 UDP 通信以及客户端是否位于 NAT 后?

客户端建立 UDP socket 然后用这个 socket 向服务器 1 的 (IP1:Port1 )发送数据, 服务器 1 收到请求后往客户端发送客户端 NAT 的 IP 和 Port(为了防止 UDP 丢包,此过程最好重复几次)。如果客户端未收到服务器的响应,则说明客户端无法进行 UDP 通信,可能是防火墙或 NAT 阻止 UDP 通信,这样的客户端也就 不能 P2P 了(检测停止)。

第二步:检测客户端 NAT 是否是 Full Cone NAT?

客户端建立 UDP socket 然后用这个 socket 向服务器 1 的 (IP1:Port1 )发送数据,服务器 1 获得客户端 NAT 的 IP 和端口 ,让服务器 2 (IP2:Port2 ) 往用户的 NAT 端口发一个 UDP 数据包。(为了防止 UDP 丢包,此过程最好重复几次)。如果客户端无法接受到服务器 2 的回应,则说明客户端的 NAT 不是一个 Full Cone NAT,具体类型有待下一步检测 (继续)。如果能够接受到服务器 2 从(IP2:Port2 )返回的 UDP 包,则说明客户端是一个 Full Cone NAT,这样的客户端能够进行 UDP-P2P 通信(检测停止)。

第三步:检测客户端 NAT 是否是 Symmetric NAT?

客户端建立 UDP socket 然后用这个 socket 向服务器 1 的 (IP1:Port1 )发送数据,服务器 1 收到请求后往客户端发送客户端 NAT 的 IP 和 Port。 用同样的方法用一个 socket 向服务器 B 的(IP2:Port2 )发送数据包要求服务器返回客户端 NAT 的 IP 和 Port。
比 较上面两个过程从服务器返回的客户端NAT(IP:Port),如果两个过程返回的 (IP:Port) 有一项不同则说明客户端为 Symmetric NAT,这样的客户端无法进行 UDP-P2P 通信(检测停止)。否则是 Restricted Cone NAT,是否为 Port Restricted Cone NAT 有待检测 (继续)。

第四步:检测客户端 NAT 是否是 Restricted Cone NAT 还是 Port Restricted Cone NAT?

客户端建立 UDP socket 然后用这个 socket 向服务器 1 的(IP1:Port1 )发送数据,服务器 1 收到数据后,使用另一个 Port(IP1:PortOther)往客户端返回一个信息(为了防止 UDP 丢包,此过程最好重复几次)。如果客户端无法接受到服务器的回应,则说明客户端是一个 Port Restricted Cone NAT,如果能够收到服务器的响应则说明客户端是一个 Restricted Cone NAT。以上两种 NAT 都可以进行 UDP-P2P 通信。

客户端代码片段

UdpClient udpClient = new UdpClient(new IPEndPoint(IPAddress.Any, 0));
IPEndPoint serverEP_A = new IPEndPoint(IPAddress.Parse("47.96.x.x"), 5020);
IPEndPoint serverEP_B = new IPEndPoint(IPAddress.Parse("47.96.x.x"), 5021);
byte[] sendbytes = Encoding.Unicode.GetBytes("test");
IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);

//发送UDP信息到服务器A
udpClient.Send(sendbytes, sendbytes.Length, serverEP_A);

//接收服务器A返回信息,并打印NAT信息
var bytRecv1 = udpClient.Receive(ref remoteEP);
Console.WriteLine(Encoding.Unicode.GetString(bytRecv1));

//发送UDP信息到服务器B
udpClient.Send(sendbytes, sendbytes.Length, serverEP_B);
//接收服务器B返回信息,并打印NAT信息
var bytRecv2 = udpClient.Receive(ref remoteEP);
Console.WriteLine(Encoding.Unicode.GetString(bytRecv2));

服务端代码片段

UdpClient udpServer = new UdpClient(5020);

Console.WriteLine("server is open on 5020");

IPEndPoint remoteEP = new IPEndPoint(IPAddress.Any, 0);
while (true)
{

    var bytRecv = udpServer.Receive(ref remoteEP);
    string message = Encoding.Unicode.GetString(bytRecv, 0, bytRecv.Length);
    //打印客户端发来的数据
    Console.WriteLine(string.Format("{0}[{1}]", remoteEP, message));
    //返回客户端NAT信息
    byte[] sendbytes = Encoding.Unicode.GetBytes("你的NAT信息 :" + remoteEP.ToString());
    udpServer.Send(sendbytes, sendbytes.Length, remoteEP);

}

UDP NAT 穿透

对于两台都隐藏在 NAT 后的 PC ,想要做 P2P 通信,第一步就是要做 NAT 穿透 (UDP 打洞).

NAT 穿透 (UDP 打洞) : 在锥形 NAT 环境下,服务器不能直接发送信息给内网 PC,但是当内网 PC 往服务器发送了一次 UDP 包后,NAT 识别到这个 UDP 包的目标地址为服务器, NAT 服务为 服务器->内网 PC 这个流程打开了一条通道, 这时候服务器再往这个 NAT 地址发送 UDP 包,NAT 会帮忙转发到内网的 PC 上,这个过程叫做 NAT 穿透 (UDP 打洞).

我们可以结合之前的 NAT 流程图再看下这个过程:

那想要进行客户端对客户端的 P2P 应该怎么做呢?

  1. 客户端 1 发送 UDP 包 到 服务器,服务器记录 客户端 1 的外网地址和端口(IP1:Port1),客户端 2 发送 UDP 包 到 服务器,服务器记录 客户端 2 的外网地址和端口(IP2:Port2)
  2. 服务器 往客户端 1 发送客户端 2 的外网 IP 和端口号(IP2:Port2), 往客户端 2 发送客户端 1 的外网 IP 和端口号(IP1:Port1)
  3. 客户端 1 往客户端 2 (IP2:Port2) 发起一次 UDP 通信,客户端 1 的 NAT 服务为 客户端 2-> 客户端 1 打开了一个通道
  4. 客户端 1 往客户端 1 (IP1:Port1) 发起一次 UDP 通信,客户端 2 的 NAT 服务为 客户端 1-> 客户端 2 打开了一个通道

经过上面 4 步, 客户端 1 和 2 就真正建立了 P2P 的连接 可以愉快的 进行 P2P 通信了.

Github 源码地址

https://github.com/sunshine4me/P2PDiscover

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!
共收到 3 条回复 时间 点赞

终于完成了, 测试代码比较零碎,我正在整理一个可以直接拿来用的版本,这周末会同步到 github.

博主你好,我有一个疑问。当多个客户机通过同一个 NAPT 向同一个服务器发送通信的时候,这个服务器返回的目标 ip 是公网的 ip 地址,那么 NAPT 此时怎么知道收到的通信包应该给内网的哪个客户机呢?

需要 登录 后方可回复, 如果你还没有账号请点击这里 注册