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

心向东 · January 16, 2018 · Last by Starnny replied at April 08, 2020 · 3802 hits

最近突然对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此时怎么知道收到的通信包应该给内网的哪个客户机呢?

需要 Sign In 后方可回复, 如果你还没有账号请点击这里 Sign Up