TCP打洞技术

原文地址:http://www.cnblogs.com/allexw/archive/2012/12/25/2832612.html

建立穿越NAT设备的p2p的TCP连接只比UDP复杂一点点,TCP协议的“打洞”从协议层来看是与UDP 的“打洞”过程非常相似的。尽管如此,基于TCP协议的打洞至今为止还没有被很好的理解,这也 造成了对其提供支持的NAT设备不是很多。在NAT设备支持的前提下,基于TCP的“打洞”技术实际上 与基于UDP的“打洞”技术一样快捷、可靠。实际上,只要NAT设备支持的话,基于TCP的p2p技术 的健壮性将比基于UDP的技术的更强一些,因为TCP协议的状态机给出了一种标准的方法来精确的 获取某个TCP session的生命期,而UDP协议则无法做到这一点。
1 套接字和TCP端口的重用
实现基于TCP协议的p2p“打洞”过程中,最主要的问题不是来自于TCP协议,而是来自于来自于应用 程序的API接口。这是由于标准的伯克利(Berkeley)套接字的API是围绕着构建客户端/服务器程序 而设计的,API允许TCP流套接字通过调用connect()函数来建立向外的连接,或者通过listen()和 accept函数接受来自外部的连接,但是,API不提供类似UDP那样的,同一个端口既可以向外连接, 又能够接受来自外部的连接。而且更糟的是,TCP的套接字通常仅允许建立1对1的响应,即应用程 序在将一个套接字绑定到本地的一个端口以后,任何试图将第二个套接字绑定到该端口的操作都会 失败。
为了让TCP“打洞”能够顺利工作,我们需要使用一个本地的TCP端口来监听来自外部的TCP连接,同时 建立多个向外的TCP连接。幸运的是,所有的主流操作系统都能够支持特殊的TCP套接字参数,通常 叫做“SO_REUSEADDR”,该参数允许应用程序将多个套接字绑定到本地的一个endpoint(只要所有要 绑定的套接字都设置了SO_REUSEADDR参数即可)。BSD系统引入了SO_REUSEPORT参数,该参数用于区分 端口重用还是地址重用,在这样的系统里面,上述所有的参数必须都设置才行。
2 打开p2p的TCP流
假定客户端A希望建立与B的TCP连接。我们像通常一样假定A和B已经与公网上的已知服务器S建立了TCP 连接。服务器记录下来每个联入的客户端的公网和内网的endpoints,如同为UDP服务的时候一样。 从协议层来看,TCP“打洞”与UDP“打洞”是几乎完全相同的过程。
1)、客户端A使用其与服务器S的连接向服务器发送请求,要求服务器S协助其连接客户端B。 2)、S将B的公网和内网的TCP endpoint返回给A,同时,S将A的公网和内网的endpoint发送给B。 3)、客户端A和B使用连接S的端口异步地发起向对方的公网、内网endpoint的TCP连接,同时监听 各自的本地TCP端口是否有外部的连接联入。 4)、A和B开始等待向外的连接是否成功,检查是否有新连接联入。如果向外的连接由于某种网络 错误而失败,如:“连接被重置”或者“节点无法访问”,客户端只需要延迟一小段时间(例如 延迟一秒钟),然后重新发起连接即可,延迟的时间和重复连接的次数可以由应用程序编写者 来确定。 5)、TCP连接建立起来以后,客户端之间应该开始鉴权操作,确保目前联入的连接就是所希望的 连接。如果鉴权失败,客户端将关闭连接,并且继续等待新的连接联入。客户端通常采用 “先入为主”的策略,只接受第一个通过鉴权操作的客户端,然后将进入p2p通信过程不再继续 等待是否有新的连接联入。

  (图 7)

与UDP不同的是,使用UDP协议的每个客户端只需要一个套接字即可完成与服务器S通信, 并同时与多个p2p客户端通信的任务,而TCP客户端必须处理多个套接字绑定到同一个本地 TCP端口的问题,如图7所示。
现在来看更加实际的一种情景,A与B分别位于不同的NAT设备后面,如图5所示,并且假定图中 的端口号是TCP协议的端口号,而不是UDP的端口号。图中向外的连接代表A和B向对方的内网 endpoint发起的连接,这些连接或许会失败或者无法连接到对方。如同使用UDP协议进行“打洞” 操作遇到的问题一样,TCP的“打洞”操作也会遇到内网的IP与“伪”公网IP重复造成连接失败或者 错误连接之类的问题。
客户端向彼此公网endpoint发起连接的操作,会使得各自的NAT设备打开新的“洞”允许A与B的 TCP数据通过。如果NAT设备支持TCP“打洞”操作的话,一个在客户端之间的基于TCP协议的流 通道就会自动建立起来。如果A向B发送的第一个SYN包发到了B的NAT设备,而B在此前没有向 A发送SYN包,B的NAT设备会丢弃这个包,这会引起A的“连接失败”或“无法连接”问题。而此时, 由于A已经向B发送过SYN包,B发往A的SYN包将被看作是由A发往B的包的回应的一部分, 所以B发往A的SYN包会顺利地通过A的NAT设备,到达A,从而建立起A与B的p2p连接。
3 从应用程序的角度来看TCP“打洞”
从应用程序的角度来看,在进行TCP“打洞”的时候都发生了什么呢?假定A首先向B发出SYN包, 该包发往B的公网endpoint,并且被B的NAT设备丢弃,但是B发往A的公网endpoint的SYN包则 通过A的NAT到达了A,然后,会发生以下的两种结果中的一种,具体是哪一种取决于操作系统 对TCP协议的实现:
(1)A的TCP实现会发现收到的SYN包就是其发起连接并希望联入的B的SYN包,通俗一点来说 就是“说曹操,曹操到”的意思,本来A要去找B,结果B自己找上门来了。A的TCP协议栈因此 会把B做为A向B发起连接connect的一部分,并认为连接已经成功。程序A调用的异步connect() 函数将成功返回,A的listen()等待从外部联入的函数将没有任何反映。此时,B联入A的操作 在A程序的内部被理解为A联入B连接成功,并且A开始使用这个连接与B开始p2p通信。
由于收到的SYN包中不包含A需要的ACK数据,因此,A的TCP将用SYN-ACK包回应B的公网endpoint, 并且将使用先前A发向B的SYN包一样的序列号。一旦B的TCP收到由A发来的SYN-ACK包,则把自己 的ACK包发给A,然后两端建立起TCP连接。简单的说,第一种,就是即使A发往B的SYN包被B的NAT 丢弃了,但是由于B发往A的包到达了A。结果是,A认为自己连接成功了,B也认为自己连接成功 了,不管是谁成功了,总之连接是已经建立起来了。
(2)另外一种结果是,A的TCP实现没有像(1)中所讲的那么“智能”,它没有发现现在联入的B 就是自己希望联入的。就好比在机场接人,明明遇到了自己想要接的人却不认识,误认为是其它 的人,安排别人给接走了,后来才知道是自己错过了机会,但是无论如何,人已经接到了任务 已经完成了。然后,A通过常规的listen()函数和accept()函数得到与B的连接,而由A发起的向 B的公网endpoint的连接会以失败告终。尽管A向B的连接失败,A仍然得到了B发起的向A的连接, 等效于A与B之间已经联通,不管中间过程如何,A与B已经连接起来了,结果是A和B的基于TCP协议 的p2p连接已经建立起来了。
第一种结果适用于基于BSD的操作系统对于TCP的实现,而第二种结果更加普遍一些,多数linux和 windows系统都会按照第二种结果来处理。


补:

原文地址:http://bbs.cnhonker.com/forum.php?mod=viewthread&tid=7479

 好了下面我们讨论TCP协议的P2P技术,TCP协议的通讯不存在UDP协议的两个问题,当然也有它的缺点,就是对网络的开销比较大。


           虽然TCP协议的P2P技术在网络上的资料不太多,但其实TCP协议的内网打洞和UDP也差不多,只是有少许不同,这里我简单做下介绍。(TCP协议的内网打洞我现在还是写不出来,只是有一个问题没有解决,就是端口重用问题,希望大牛可以给我指点一下



           和上面的图一样,我们要实现主机1和主机2直接的TCP协议的P2P通讯。下面的步骤资料来自于网络(略有删改)

           主机1和主机2 进行通讯的步骤
           1.服务器启动两个网络侦听,一个叫【主连接】侦听,一个叫【协助打洞】的侦听。 
           2. 主机1和主机2 分别与服务器的【主连接】保持联系。
           3.当主机1需要和主机2 建立P2P的TCP 连接时,首先连接服务器的【协助打洞】端口(此时NAT1已经自动打了一个“洞”),并发送协助连接申请。同时在本机连接到服务器【协助打洞】端口的端口号上启动侦听。注意由于要在相同的网络终端 上绑定到不同的套接字上,所以必须为这些套接字设置 SO_REUSEADDR 属性(即 允许重用),否则侦听会失败。
            4. 服务器的【协助打洞】连接收到主机1 的申请后通过【主连接】通知主机2,并将主机1 经过 NAT1 转换后的公网 IP 地址和端口等信息告诉主机2。
            5.主机2收到服务器 的连接通知后首先与服务器的【协助打洞】端口连接(此时NAT2已经自动打了一个“洞”),随便发送一些数 据后立即断开,这样做的目的是让服务器能知道主机2 经过 NAT2 转换后的公网 IP 和端 口号。
            6.主机2尝试与主机1的经过 NAT1转换后的公网 IP 地址和端口进行 connect,根据不 同的路由器会有不同的结果,有些路由器在这个操作就能建立连接,大多数路由器对于不请自到的 SYN 请求包直接丢弃而导致 connect 失败,但 NAT1会纪录此次连接的源地址和端口号,为接下来真正的连接做好了准备,下次主机1 就能直接连接到主机2刚才使用的端口号了。
            7.主机2打洞的同时在相同的端口上启动侦听。主机2 在一切准备就绪以后通过 与服务器的【主连接】回复消息“我已经准备好”,服务器在收到以后将主机2 经过 NAT2 转 换后的公网 IP 和端口号告诉给主机1
            8. 主机1 收到 服务器回复的主机2 的公网 IP 和端口号等信息以后,开始连接到 主机2 公网 IP 和 端口号,由于在步骤 6 中 主机2 曾经尝试连接过 A 的公网 IP 地址和端口,NAT1 纪录 了此次连接的信息,所以当 主机1 主动连接 主机2 时,NAT2 会认为是合法的 SYN 数 据,并允许通过,从而直接的 TCP 连接建立起来了。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值