P2P技术原理解析

发布时间 2023-11-07 00:10:58作者: antiGod

一、为什么我们的电脑不能直接上外网

现在大多数设备的网络采用的是ipv4网络,ipv4中ip的定义为x.x.x.x,其中每一位为0-255,所以全球的ip总数256^4=4294967296(42亿多个IP地址),这个数量是不足够全球使用的,为了让每个人都有IP可用,计算机网络模型采取了一个单位(若干台计算机)共用一个公网IP,这个单位内部每台计算机都分配一个内网IP地址,内网计算机向外网发起请求时,由路由器将内网地址转换为公网地址(也就是网络地址转换,简称NAT(Network Address Translation)),路由器和他后面的内网计算机就组成了一个局域网。因此局域网内部的计算机因为只有内网地址而没有公网地址,所以不能直接连上外网,需要通过路由器转换内网地址为公网地址才能接上外网,而公网地址是我们接入电信网络时由电信服务商提供的,可以是静态IP地址,但是现在大部分都是动态分配的。

因为IPv4就是造成我们电脑不能直接接上外网的原因,而IPv6的地址空间非常大,ip总数也大得多,总共有2的128次方个IP地址,约等于3.4×10的38次方个地址。这意味着,IPv6可以为每个人、每个设备、每个物品分配多个IP地址,远远超过了IPv4的42亿个地址,据说应用IPv6后,地球上每一粒沙子都可以分配一个IP地址。因此,IPv6被广泛认为是未来互联网的基础协议 。

那为什么不一开始就直接使用IPv6而使用了IPv4了,当然这个是历史原因造成的,如今想要全面升级到IPv6的话,很多网络设备都不支持,还有操作系统和各种软件等等,还有需要处理好各种网络安全等等方面,因此全面升级IPv6是一个大的体系工程,需要全社会共同支持和努力

二、P2P是什么

p2p是点对点的缩写(peer-to-peer networking),其可以定义为:端对端的资源共享,每一端即可是服务端,也可以是客户端。既可以是资源的提供者,也可以是资源的共享者。传统C/S模型需要实现端和端的资源共享, 需要将资源上传到服务器,然后另外一端再从转服务器下载,如下图所示:

 

P2P则不需要将资源上传到服务器,它是端对端传输,每一个端点既可以是服务器,也可以是客户端。采用p2p之后,端点之间的实时性最高,无需服务器中转,不占服务器宽带,通信更加安全。在视频直播,在线教育,视频监控,安防行业用的比较多。p2p架构如下图所示:

 

p2p模式支持同一个内网的端点之间直接互传数据,如上图的客户端B和客户端C,这种方式数据传输是最短最快的。

但这种方面面临一个最大的问题,就是路由器会将外面连接进来的请求拒绝,导致无法连接的问题。

路由器允许内网计算机向外发起连接请求,比如上面的客户端A向服务器发起连接请求,经过NAT1时,NAT会将内网IP和端口转为为外网IP地址和映射为一个新的端口,在路由器上会建立一个内网地址端口和外网地址端口的映射,通过这个地址和服务器建立连接,然后服务器才可以发送数据到客户端A,NAT1会检查路由器映射表是否有这个地址和端口,有则允许数据发送到客户端A。相反如果客户端A未建立与服务器的这个连接,服务器直接发送数据到客户端A,NAT1在路由器映射表里面查找不到这个地址端口的映射,会被NAT1拒绝丢弃,这个是为了网络安全考虑的。同理客户端A要直接连接客户端B,会被NAT2拒绝丢弃,客户端B要连接客户端A,同样会被NAT1所拒绝丢弃。

下面将介绍下NAT技术。

三、NAT是什么

NAT全称是网络地址转换(Network Address Translation),它是一种把内部私有网络地址(IP地址)转换成公网网络IP地址的技术。比如我们电脑里面网卡地址是192.168.1.10,但是我们再百度搜索“我的IP”却显示210.12.214.53,这就是NAT的功能。NAT主要是部署在路由器或者交换机上。

NAT有4个类型,它们分别是:NAT1、NAT2、NAT3、NAT4。从 NAT1 至 NAT4 限制越来越多。

  1. 完全圆锥形NAT(Full Cone NAT)

    完全圆锥型NAT把一个来自内部IP地址和端口的所有请求,始终映射到相同的外网IP地址和端口;同时,任意外部主机向该映射的外网IP地址和端口发送报文,都可以实现和内网主机进行通信,就像一个向外开口的圆锥形一样,故得名。

    这种模式很宽松,限制小,只要内网主机的IP地址和端口与公网IP地址和端口建立映射关系,所有互联网上的主机都可以访问该NAT之后的内网主机。

     

  2. 地址限制式圆锥形NAT(Address Restricted Cone NAT)

    地址限制式圆锥形NAT同样把一个来自内部IP地址和端口的所有请求,始终映射到相同的外网IP地址和端口;与完全圆锥型NAT不同的是,当内网主机向某公网主机发送过报文后,只有该公网主机才能向内网主机发送报文,故得名。相比NAT1,NAT2 增加了地址限制,也就是IP受限,而端口不受限。

    如下图所示,地址限制圆锥型NAT(Address Restricted Cone NAT)会将客户机地址{X:y}转换成公网地址{A:b}并绑定,只有来自主机{P}的包才能和主机{X:y}通信。

     

  3. 端口限制式圆锥形NAT(Port Restricted Cone NAT)

    端口限制式圆锥形NAT更加严格,在上述条件下,只有该公网主机该端口才能向内网主机发送报文,故得名。相比NAT2,NAT3 又增加了端口限制,也就是说IP、端口都受限。

    端口限制圆锥型NAT(Port Restricted Cone NAT)会将客户机地址{X:y}转换成公网地址{A:b}并绑定,只有来自主机{P,q}的包才能和主机{X:y}通信。如下图所示:

     

  4. 对称式NAT(Symmetric NAT)

    对称式NAT把内网IP和端口到相同目的地址和端口的所有请求,都映射到同一个公网地址和端口;同一个内网主机,用相同的内网IP和端口向另外一个目的地址发送报文,则会用不同的映射(比如映射到不同的端口)。如下图所示:

     

    对称型NAT(Symmetric NAT)会将客户机地址{X:y}转换成公网地址{A:b}并绑定为{X:y}|{A:b}<->{P:q}。对称型NAT只接受来自{P:q}的incoming packet,将它转给{X:y} ,每次客户机请求一个不同的公网地址和端口,NAT会新分配一个端口号{C,d} 。

    和端口限制式NAT不同的是,端口限制式NAT是所有请求映射到相同的公网IP地址和端口,而对称式NAT是为不同的请求建立不同的映射。

    它具有端口受限锥型的受限特性,内部地址每一次请求一个特定的外部地址,都可能会绑定到一个新的端口号。也就是请求不同的外部地址映射的端口号是可能不同的。

四、P2P打洞(内网穿透)

我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。但是如果外部的计算机想访问子网内的计算机就比较困难了,比如ClientA连接ClientB或者ClientB连接ClientA。

那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个内网地址端口和外网地址端口的映射),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机ClientA向外部的某个IP(比如ClientB的外网地址端口)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为ClientB的“洞”,(这就是称为UDP Hole Punching的技术),以后ClientB就可以通过这个洞与内网的ClientA联系了。(但是其他的IP不能利用这个洞)。

P2P打洞的步骤:

  1. Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。

  2. Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的ClientB地址是187.34.1.56:40000。

  3. 如果Client A此时想直接发送信息给ClientB,那么他可以从Server S那儿获得ClientB的公网地址187.34.1.56:40000,同时Server S把A的外网地址和端口发回给ClientB(如果此时ClientA直接向ClientB连接发消息肯定会失败的,因为NATB上并没有ClientB到ClientA的映射,NATB认为不是一个安全数据会丢弃数据包)。

  4. ClientB向ClientA的外网地址202.187.45.3:60000发送连接请求,NATA会很当然的拒绝丢弃数据包,但同时在NATB上建立了ClientB到ClientA的映射了;

  5. 最后ClientA向ClientB发送连接请求,由于上一步已经在NATB上建立ClientB到ClientA的映射了,因此NATB自然会放行,将数据交给ClientB,于是传输通道建立,后面就可以发送数据了。

    注意:以上过程只适合于Cone NAT的情况,如果是Symmetric NAT,那么当Client B向Client A打洞的端口已经重新分配了,Client B将无法知道这个端口(如果Symmetric NAT的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。

五、P2P打洞的具体实现

  1. 假设两个客户端A和B要进行p2p,首先需要将两个客户端的信息都传到服务器上,服务器上表示客户端的结构如下:

    public class ClientInfo {

    private String userId;

    private String clientIP;

    private Integer clientPort;

    .........................

    }

  2. ClientA和ClientB都连接上服务器后,服务器能拿到两端的外网IP和端口,放到一个Map中保存起来。由客户端发起连接请求,比如B请求连接A,那么服务器就将A的外网IP和Port发给B,将B的外网IP和Port发给A,这样A和B就都有了对方的外网IP和端口;

  3. A和B都直接连接对方,因为线程有先后顺序,比如A先连接的B,肯定会失败,但是在NATA上会建立与B的映射,这时B再连接A,因为NATA上有映射关系,所以B连接A能连接上,然后就可以发消息数据了。这里需要注意的是服务器在完成交换客户端公网IP和端口后,两个客户端需要分别关闭与服务器的连接,创建新的Socket,设置SO_REUSEADDR标记为True,记录下之前连接服务器的端口,现在创建的socket还是绑定在这个端口。

    时序图如下:

     

六、关键代码实现

  1. 服务器绑定8080端口:

     

  2. 获取当前所有已连接客户的信息

     

  3. 将A的信息发给B

     

  4. Center通知B:"A已知道你准备好了,你可以连接A了"

     

  5. B新建一个Socket与Center建立连接,告诉中央:这个连接,是为UUID的A连接做准备的

     

  6. 建立P2P连接

     

     

     

七、参考资料

https://blog.csdn.net/xiehuanbin/article/details/129659184

https://gitee.com/snltty/p2p-tunnel?_from=gitee_search

https://www.cppblog.com/Lee7/archive/2008/01/25/41850.html

https://gitee.com/coderlang/p2p-demo

https://blog.csdn.net/cout__waht/article/details/78735737