最近突然对 P2P 的实现感了兴趣,所以在查阅了一些资料后,自己实现了最简单的 P2P 功能。正好许久没来社区码字,在这里记录和分享一下过程中的知识点。
因为主要作为记录用,所以文中提到的相关知识点只会讲述个大概不会大篇幅展开,肯定会有所纰漏,想了解详细信息的同学可以自行查阅相关资料。
说到 P2P 呢大家应该都听说过, 生活中其实很多应用都用到了这项技术,比如某些即时通信软件、某些需要实现多人联机的游戏都或多或少的会使用这项技术。
点对点通信最简单的实现就是两台计算机在知道对方 IP 地址的情况下,直接连接对方端口并发送数据。如果在局域网中这一条件非常容易达成,但在互联网中因为众所周知的 ipv4 地址短缺而 ipv6 迟迟不能普及等问题,每台计算机根本无法直接分配到一个外网 IP,没有明确的 IP 地址,两台计算机就无法直接进行点对点通信。
关于ipv4和ipv6其实也是个很有意思的话题,但是我这边篇幅有限就不详细展开了,想了解的可以查阅网上其他的资料
想要了解如何在互联网中实现 P2P,必须知道很多相关的知识点,下面我一点点讲解。
举例 : 你在家使用电信、网通或者其他宽带服务时,你的个人 PC 所分配到的 IP 地址叫内网 IP 地址(一般为 192.168.x.x)。当你访问一个网站时,网站服务器看到你的来源 IP 并不是你内网 IP 地址,而是一个公网 IP 地址(由网络服务器商提供)。
那为什么会有公网 IP和内网 IP呢, 那就要引出下一个知识点了。
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 的类型知识点了。
Full Cone NAT(全锥 NAT): 全锥 NAT 把所有来自相同内部 IP 地址和端口的请求映射到相同的外部 IP 地址和端口。任何一个外部主机均可通过该映射发送数据包到该内部主机
Restricted Cone NAT(限制性锥 NAT): 限制性锥 NAT 把所有来自相同内部 IP 地址和端口的请求映射到相同的外部 IP 地址和端口。但是, 和全锥 NAT 不同的是:只有当内部主机先给外部主机发送数据包, 该外部主机才能向该内部主机发送数据包
Port Restricted Cone NAT(端口限制性锥 NAT): 端口限制性锥 NAT 与限制性锥 NAT 类似, 只是多了端口号的限制, 即只有内部主机先向外部地址:端口号对发送数据包, 该外部主机才能使用特定的端口号向内部主机发送数据包。
Symmetric NAT(对称 NAT): 对称 NAT 与上述 3 种类型都不同, 不管是全锥 NAT ,限制性锥 NAT 还是端口限制性锥 NAT ,它们都属于锥 NAT(Cone NAT )。当同一内部主机使用相同的端口与不同地址的外部主机进行通信时, 对称 NAT 会重新建立一个 Session ,为这个 Session 分配不同的端口号,或许还会改变 IP 地址。
对于实现 P2P 通信来说,各种 NAT 类型的实现难度依次为 对称型 > 端口受限锥型 > 受限锥型 > 全锥型,所以我们在实现 P2P 通信前还需要测试我们所处的 NAT 环境属于哪种类型。
前提条件: 有两个公网的 IP 地址(IP-1,IP-2),并在开启两个 UDP 端口监听 (IP-1,Port-1),(IP-2,Port-2)。
顺便提一下,做这个测试的时候需要两台带外网 IP 的服务器,我个人只有 1 台测试用的外网服务器,得想办法再搞一台。我从阿里云那申请了一台时间为一周的突发性实例,只用了 15 块,如果大家想自己测试的话 完全可以花 30 快租个两台机器进行测试。
现在我有两台服务器,并开启两个服务 (IP1:Port1 )和 (IP2:Port2 )
客户端建立 UDP socket 然后用这个 socket 向服务器 1 的 (IP1:Port1 )发送数据, 服务器 1 收到请求后往客户端发送客户端 NAT 的 IP 和 Port(为了防止 UDP 丢包,此过程最好重复几次)。如果客户端未收到服务器的响应,则说明客户端无法进行 UDP 通信,可能是防火墙或 NAT 阻止 UDP 通信,这样的客户端也就 不能 P2P 了(检测停止)。
客户端建立 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 通信(检测停止)。
客户端建立 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 有待检测 (继续)。
客户端建立 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);
}
对于两台都隐藏在 NAT 后的 PC ,想要做 P2P 通信,第一步就是要做 NAT 穿透 (UDP 打洞).
我们可以结合之前的 NAT 流程图再看下这个过程:
那想要进行客户端对客户端的 P2P 应该怎么做呢?
经过上面 4 步, 客户端 1 和 2 就真正建立了 P2P 的连接 可以愉快的 进行 P2P 通信了.