P2P之UDP穿透NAT的原理与实现 & Skype(已整理过)

P2P 之UDP穿透NAT的原理与实现(附源代码)
原创:shootingstars
参考:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt

论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念
    NAT(NetworkAddressTranslators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类:

          * 基本的NAT,和

          * NAPT(Network Address/Port Translator)。


   最开始NAT是运行在路由器上的一个功能模块。
   
   最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。
   因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口).
    关于基本的NAT,可以参看RFC 1631.
   
   另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口号。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:

                               ServerS1                        
                        18.181.0.31:1235                         
                                     |
         ^  Session 1 (A-S1) ^     | 
         |  18.181.0.31:1235 |     |  
         v 155.99.25.11:62000v     |   
                                     |
                                    NAT
                                155.99.25.11
                                     |
         ^  Session 1 (A-S1) ^     | 
         |  18.181.0.31:1235 |     | 
         v  10.0.0.1:1234   v     | 
                                     |
                                  Client A
                               10.0.0.1:1234

   有一个私有网络10.*.*.*,ClientA是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDPSocket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
   首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。

   一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动地转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样ClientA就与Server S1建立以了一个连接。

   呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。
   看看下面的情况:
    ServerS1                                    Server S2
 18.181.0.31:1235                             138.76.29.7:1235
       |                                            |
       |                                            |
       +----------------------+----------------------+
                              |
   ^  Session 1(A-S1) ^     |     ^  Session 2 (A-S2)  ^
   | 18.181.0.31:1235 |     |     |  138.76.29.7:1235  |
   v 155.99.25.11:62000v     |     v 155.99.25.11:62000 v
                              |
                           Cone NAT
                         155.99.25.11
                              |
   ^  Session 1(A-S1) ^     |     ^  Session 2 (A-S2)  ^
   | 18.181.0.31:1235 |     |     |  138.76.29.7:1235  |
  v  10.0.0.1:1234   v     |     v  10.0.0.1:1234   v
                              |
                           Client A
                        10.0.0.1:1234
   接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个ServerS2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?
   这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做SymmetricNAT,后一种叫做ConeNAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(庆幸的是,现在绝大多数的NAT属于后者,即ConeNAT)
  
   好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于是透明的,子网内的和外网的计算机不用知道NAT的情况)。

   但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。

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

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。

   现在我们来看看一个P2P软件的流程,以下图为例:

                      Server S (219.237.60.1)
                         |
                         |
  +----------------------+----------------------+
  |                                            |
 NAT A(外网IP:202.187.45.3)                NAT B (外网IP:187.34.1.56)
  |  (内网IP:192.168.0.1)                     | (内网IP:192.168.0.1)
  |                                            |
Client A (192.168.0.20:4000)            Client B (192.168.0.10:40000)


 
    首先,ClientA登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S 收到的ClientA的地址是202.187.45.3:60000,这就是Client A的外网地址了。
      同样,ClientB登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。
      此时,ClientA与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从ServerS那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息ClientB就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃( 因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NATB上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么ClientA发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢? 过程如下:

                  概念: 私有地址/端口和公有地址/端口
            我们知道,现在大部分网络采用的都是NAPT(Network Address/Port Translator)了,这个东东的作用是一个对外的对话在经过NAT之后IP地址和端口号都会被改写,在这里,我们把一次会话中客户自己认为在使用的IP地址和端口号称为私有地址/端,而把经过NAPT之后被改写的IP地址和端口号称为公有地址/端口.

             客户端首先得到自己的私有地址/终端,然后向server端发送登陆请求,server端在得到这个请求之后就可以知道这个client端的公有地址/终端,server会为每一个登陆的client保存它们的私有地址/端口和公有地址/端口.

             OK,下面开始关键的打洞流程. 假设client A要向client B 对话, 但是A不知道B的地址,即使知道,根据NAT的原理,这个对话在第一次会被拒绝, 因为client B的NAT认为这是一个从没有过的外部发来的请求. 这个时候, A如果发现自己没有保存B的地址, 或者说发送给B的会话请求失败了,它会要求server端通知B向A打一个洞,这个B->A的会话意义在于它使NAT B认为A的地址/端口是可以通过(即已被认可)的地址/端口,这样A再向B发送对话的时候就不会再被NAT B拒绝了.打一个比方来说明打洞的过程, A想来B家做客,但是遭到了B的管家NAT B的拒绝,理由是:我从来没有听我家B提过你的名字, 这时A找到了A和B都认识的朋友server, 要求server给B报一个信,让B去跟管家说A是我的朋友。于是,B跟管家NAT B说,A是我认识的朋友,这样A的访问请求就不会再被管家NAT B所拒绝了。简而言之,UDP打洞就是一个通过server保存下来的地址使得彼此之间能够直接通信的过程,server只管帮助建立连接,在建立间接之后就不再介入了。



      总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求ServerS 命令Client B 向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密8)),然后Client A就可以通过Client B的外网地址与Client B通信了。
     

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

     
      下面是一个模拟P2P聊天的过程的源代码,过程很简单:P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopbacktranslation,详见 http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt)。当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过sendusername message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。



Skype的通信原理

  1. Skype网络结构

  与常规的电信业务网络不同的是,Skype的网络中除了注册服务器,没有其他任何集中的服务器,只是将用户节点分为普通节点和超级节点。
  注册服务器是Skype惟一需要维护的设备,它负责完成客户端的注册,存储并管理用户名和密码信息,当用户登录系统时,对用户进行身份认证。注册服务器还需要检验并保证用户名的全球惟一性。
  普通节点即普通主机终端,只需要下载了Skype的应用,就具有提供语音呼叫和文本消息传送的能力。
      超级节点实际上是满足某些要求的普通节点,这些要求包括:具有公网地址、具有足够的CPU、存储空间足够大、具有足够的网络带宽。也就是说,任何符合条件的主机终端都可以成为超级节点,当然前提是加载了Skype应用。

  2. 通信流程

  Skype的通信流程分为:启动、注册(认证)、查找用户、呼叫和释放的过程。其中注册流程只是在用户初次安装了Skype的客户端软件后进行注册,后期使用的过程中该步骤就变成认证过程。

  (1)启动流程
  Skype的用户终端启动时,采用HTTP协议连接到注册服务器,用户初次安装的启动流程中携带“installed”的参数,使用时启动流程则在消息中携带“getlatestversion”参数,具体流程分别见图3和图4。




  (2)注册(认证)流程
  注册(认证)流程可能是Skype所有流程中最复杂的一个,用户启动Skype后,首先需要连接到超级节点,通过超级节点向注册服务器发送身份认证信息,注册服务器验证用户名和密码的合法性,然后向其他对等节点及其好友发送在线信息,同时还需要判断该终端所在私网的NAT和防火墙类型。如果该终端先前默认的超级节点已不可用,则还要查找具有公网地址的Skype节点来作为该终端的超级节点,从而维持该终端与Skype网络的连接。

  一旦超级节点都不可用,Skype的客户端采用了尽力而为的方式进行注册,即先用UDP包试注册,不成功超时则用TCP(80端口),再不成功则用TCP(443端口)。通常为防止其超级节点不可用,客户端必须建立一个可选连接节点列表,并定期维护该列表。

  对于该流程通常会产生下面几点疑问:

  ● 初次登录时如何连接到超级节点?

  我们发现,用户初次安装了Skype客户端软件后,该客户端的主机缓存中就已经初始化了一个包含至少7个IP地址和端口组的列表,这些地址和端口组所代表的便是初始的超级节点。

  ● 如何向好友发送在线信息?

  由于Skype采用路由缓存机制,即用户查找其好友的过程中会在中间的超级节点缓存其路由信息(缓存72小时),因此用户登录后,其状态信息可以通过其超级节点通知到好友终端,并将好友的状态返回给用户。一旦缓存超时,需要通过其他超级节点查找用户路由,这充分体现了Skype的用户路由信息动态分布式存储的特点。

  ● 如何判断NAT和防火墙类型?

  客户端软件采用各种STUN协议与超级节点之间交换信息,从而判断终端所处私网的NAT和防火墙类型,客户端软件还采用定期刷新机制来保证任何时候都能穿越NAT和防火墙。

  (3)查找用户

  Skype采用了一种称做全球索引(GlobalIndex)的技术来查找用户,该技术结合前面所述的分层网络,超级节点之间采用全分布式的连接,每个超级节点具有最小时延前提下所有可用的用户和资源的全部信息。具体来说,Skype采用了下面两种机制来保证顺利完成用户的查找。

  ● 启动后向所有列表中的用户发送其上线信息,其他用户响应各自的信息;

  ● 在中间节点缓存查找到的用户信息

  对于有公网地址的客户端,其查找用户的过程如下:点击发送要查找的用户信息->通过SN获取四个节点地址->不成功->报告SN->获取八个节点地址->……->成功(或失败返回)。

  对于那些位于私网内的受限客户端,其查找用户的过程则是首先客户端将需要查找的用户信息发送给其SN,然后由SN完成查找后返回给私网内的客户端。

  (4)呼叫建立和释放

  查找到希望连接的用户后,可以将其加入好友列表,Skype用户可以随时与在线的好友进行呼叫。经过了稍微复杂的认证过程和用户查找过程,呼叫建立和释放的过程就变得简单了。用户位于公网和位于私网内部的情况会有所不同,两种情况下的呼叫建立和释放流程分别见图5和图6所示。



  从上述流程图可见, Skype的呼叫信令都采用TCP封装,而媒体流则使用UDP封装,当有任何一方用户位于限制UDP包的防火墙内时,媒体流就会采用TCP封装。另外当Skype用户至少有一方位于私网内时,所有的信令和媒体消息都经过一个或多个中间节点转发。此时无需担心用户通话的媒体流因为经过中间节点转发而被窃听,因为Skype采用了对消息进行端到端加密的机制。

   Skype的技术优势

  Skype之所以引起了不小的轰动,是因为它的互联网特性,即免费、开放和较好的业务质量。事实上,Skype最大的意义在于,它开创了将P2P技术引入到话音通信的先河。也就是说,采用了网络中的所有节点都动态参与到路由、信息处理和带宽增强等工作中的机制,而不是单纯依靠服务器来完成这些工作,因此其管理成本大大降低,同时又保证了语音质量。

  从具体技术的角度来看,Skype的优势有下面几点:

  (1)较强的NAT和防火墙穿越能力。首先识别NAT和防火墙类型,然后通过动态的选择信令和媒体代理,从而轻松实现NAT和防火墙的穿越。

  (2) 快速路由机制,Skype采用了全球索引(GlobalIndex)技术提供快速路由,其用户路由信息分布式存储于网络节点中。

  (3) 结合互联网特点的语音编解码算法。Skype通过与Global IPSound公司合作,引入语音质量增强软件,专门针对互联网的特点,从而降低了业务对带宽的要求。

  (4)很低的运行成本。很显然,Skype将很多工作下放给网络节点去完成,大大地降低了中心服务器的负担,进而减少了维护和管理的成本。

  (5) 开放性。Skype采取开放的机制,鼓励互联网用户自己开发插件,目前此类开发如雨后春笋,在互联网上遍地开花。

  其中第1条保证了通信无障碍,无论终端处于何种网络条件,都不会影响用户使用Skype提供的业务。第2条和第3条则保证了Skype较好的业务服务质量。第4条使得Skype可以轻松面对挑战。而第5条则给了Skype更强大的生命力,使其更加灵活,具有更高的可扩展性。

   Skype给电信运营带来的思考

  Skype的出现无疑给传统电信业带来一股强烈的冲击波,它从2003年下半年出现以来便广为流传,截至目前,Skype全球注册用户数已达2.5亿,每天增加的会员有15.5万人,而到2005年3月14日为止,Skype在全球的通话量累计已经达到60亿分钟。Skype仍在迅速向各个国家渗透,最新的统计表明:使用Skype技术呼叫的分钟数已经占到美国VoIP分钟数的46.2%,这部分用户基本是“免费”享用电话业务的。

  Skype毕竟是一种互联网服务,而不是真正的电信服务,它也存在一些问题,比如网络的无管理性使得它只能通过这种免费的方式走向市场,企业用户也会因为担心它的安全隐患而不采用等。但是无论如何,Skype的理念很可能给传统的电信市场带来突破性的变革,也引起业界对于VoIP的新的思考,传统电信运营商决不可忽视其挑战。

  首先,对于VoIP的态度问题,该积极应对还是消极回避?答案已经非常明显——微软公司的总裁曾说过这样的话:即使没有Skype,也会有另一个提供互联网电话的企业出现。我国在上个世纪90年代末成功地开展了VoIP的电信运营,到现在市场基本稳定,普通用户对IP电话已经不再陌生,然而几年来VoIP的电信运营却没有继续发展,几乎有点停滞不前了。事实上,VoIP应该具有更广阔的空间和更灵活的表现形式。

  技术的进步往往是不可阻挡的,尤其是互联网带来的开放时代,给了新技术更广阔的发展空间,你不发展,别人会发展,因此国内电信运营商应该积极地迈出这一步。

  VoIP的发展可以采取开放的思路,Skype的成功也有一些可借鉴之处。首先,积极研究P2P技术的利弊:一旦将P2P技术引入可运营的VoIP系统,在降低运营成本、具有更高的网络可靠性(不依赖于集中的服务器)的同时,是否会带来不可预知的坏影响?另外,VoIP并不代表人们通常所理解的“低收费就一定提供低质量服务”,电信运营商可以结合IP网络的特点,引入相应的技术,来保证可靠的服务质量。


穿越NAT总结

对穿越NAT做些总结:

 

先做个约定

内网A中有:A1192.168.0.8A2192.168.0.9两用户,

网关X1(一个NAT设备)有公网IP 1.2.3.4

内网B中有:B1192.168.1.8B2192.168.1.9两用户,

网关Y1(一个NAT设备)有公网IP 1.2.3.5

公网服务器:C (6.7.8.9)  D (6.7.8.10)

 

NAT两大类:

l         NAT(Network Address Translators):称为基本的NAT

 P2P之UDP穿透NAT的原理与实现 & Skype(已整理过)_第1张图片

在客户机时

192.168.0.8:4000——6.7.8.9:8000

 

在网关时

1.2.3.4:4000——6.7.8.9:8000

服务器C

6.7.8.9:8000

其核心是替换IP地址而不是端口,这会导致192.168.0.8使用4000端口后,192.168.0.9如何处理?

具体参考RFC 1631

基本上这种类型的NAT设备已经很少了。或许根本我们就没机会见到。

l         NAPT(Network Address/Port Translators)

其实这种才是我们常说的 NAT

NAPT的特点是在网关时,会使用网关的 IP,但端口会选择一个和临时会话对应的临时端口。

如下图:

在客户机时

192.168.0.8:4000——6.7.8.9:8000

 

在网关时

1.2.3.4:62000——6.7.8.9:8000

服务器C

6.7.8.9:8000

网关上建立保持了一个1.2.3.4:62000的会话,用于192.168.0.8:40006.7.8.9:8000之间的通讯。

 

对于NAPT,又分了两个大的类型:

差别在于,当两个内网用户同时与6.7.8.9:8000的处理方式不同:

1Symmetric NAT (对称型)

P2P之UDP穿透NAT的原理与实现 & Skype(已整理过)_第2张图片

在客户机时

192.168.0.8:4000——6.7.8.9:8000    192.168.0.8:4000——6.7.8.10:8000

 

在网关时,两个不同session端口号不同

1.2.3.4:62000——6.7.8.9:8000    1.2.3.4:62001——6.7.8.10:8000

服务器C

6.7.8.9:8000

服务器 D

6.7.8.10:8000

这种形式会让很多p2p软件失灵。

2、Cone NAT型(圆锥型)

P2P之UDP穿透NAT的原理与实现 & Skype(已整理过)_第3张图片

在客户机时

192.168.0.8:4000——6.7.8.9:8000    192.168.0.8:4000——6.7.8.10:8000

 

在网关时,两个不同session端口号相同

1.2.3.4:62000——6.7.8.9:8000    1.2.3.4:62000——6.7.8.10:8000

服务器C

6.7.8.9:8000

服务器D

6.7.8.10:8000

目前绝大多数属于这种。Cone NAT又分了3种类型:

a)         Full Cone NAT(完全圆锥型):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000 192.168.0.8可以收到任意外部主机发到1.2.3.4:62000的数据报。

b)        Address Restricted Cone NAT (地址限制圆锥型):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000只有当内部主机192.168.0.8先给服务器C 6.7.8.9发送一个数据报后,192.168.0.8才能收到6.7.8.9发送到1.2.3.4:62000的数据报。

c)        Port Restricted Cone NAT(端口限制圆锥型):从同一私网地址端口192.168.0.8:4000发至公网的所有请求都映射成同一个公网地址端口1.2.3.4:62000只有当内部主机192.168.0.8先向外部主机地址端口6.7.8.98000发送一个数据报后,192.168.0.8才能收到6.7.8.98000发送到1.2.3.4:62000的数据报。

请注意上述描叙中的区别!

穿越NAT的实现:

P2P之UDP穿透NAT的原理与实现 & Skype(已整理过)_第4张图片

A1在客户机时

192.168.0.8:4000——6.7.8.9:8000

 

X1在网关时

1.2.3.4:62000——6.7.8.9:8000

服务器C

6.7.8.9:8000

B1在客户机时

192.168.1.8:4000——6.7.8.9:8000

 

Y1在网关时

1.2.3.5:31000——6.7.8.9:8000

两内网用户要实现通过各自网关的直接呼叫,需要以下过程:

1、  客户机A1B1顺利通过格子网关访问服务器C ,均没有问题(类似于登录)

2、  服务器C保存了 A1B1各自在其网关的信息(1.2.3.4:620001.2.3.5:31000)没有问题。并可将该信息告知A1B2

3、  此时A1发送给B1网关的1.2.3.5:31000是否会被B1收到?答案是基本上不行(除非Y1设置为完全圆锥型,但这种设置非常少),因为Y1上检测到其存活的会话中没有一个的目的IP或端口于1.2.3.4:62000有关而将数据包全部丢弃!

4、  此时要实现A1B1通过X1Y1来互访,需要服务器C告诉它们各自在自己的网关上建立“UDP隧道”,即命令A1发送一个192.168.0.8:4000——1.2.3.5:31000的数据报,B1发送一个192.168.1.8:4000——1.2.3.4:62000的数据报,UDP形式,这样X1Y1上均存在了IP端口相同的两个不同会话(很显然,这要求网关为Cone NAT,否则,对称型Symmetric NAT设置网关将导致对不同会话开启了不同端口,而该端口无法为服务器和对方所知,也就没有意义)。

5、  此时A1发给Y1,或者B1发给X1的数据报将不会被丢弃且正确的被对方收到

 

综合P2P可实现的条件需要:

1、  中间服务器保存信息、并能发出建立UDP隧道的命令

2、  网关均要求为Cone NAT类型。Symmetric NAT不适合。

3、  完全圆锥型网关可以无需建立udp隧道,但这种情况非常少,要求双方均为这种类型网关的更少。

4、  假如X1网关为Symmetric NAT Y1Address Restricted Cone NAT Full Cone NAT型网关,各自建立隧道后,A1可通过X1发送数据报给Y1B1(因为Y1最多只进行IP级别的甄别),但B2发送给X1的将会被丢弃(因为发送来的数据报中端口与X1上存在会话的端口不一致,虽然IP地址一致),所以同样没有什么意义。

5、  假如双方均为Symmetric NAT的情形,新开了端口,对方可以在不知道的情况下尝试猜解,也可以达到目的,但这种情形成功率很低,且带来额外的系统开支,不是个好的解决办法。

6、  不同网关型设置的差异在于,对内会采用替换IP的方式、使用不同端口不同会话的方式,使用相同端口不同会话的方式;对外会采用什么都不限制、限制IP地址、限制IP地址及端口。

7、  这里还没有考虑同一内网不同用户同时访问同一服务器的情形,如果此时网关采用Address Restricted Cone NAT Full Cone NAT型,有可能导致不同用户客户端可收到别人的数据包,这显然是不合适的。

 

 

一些现在常用的技术:

ALG(应用层网关):它可以是一个设备或插件,用于支持SIP协议,主要类似与在网关上专门开辟一个通道,用于建立内网与外网的连接,也就是说,这是一种定制的网关。更多只适用于使用他们的应用群体内部之间。

 

UpnP :它是让网关设备在进行工作时寻找一个全球共享的可路由IP来作为通道,这样避免端口造成的影响。要求设备支持且开启upnp功能,但大部分时候,这些功能处于安全考虑,是被关闭的。即时开启,实际应用效果还没经过测试。

 

STUNSimple Traversalof UDP Through Network):这种方式即是类似于我们上面举例中服务器C的处理方式。也是目前普遍采用的方式。但具体实现要比我们描述的复杂许多,光是做网关Nat类型判断就由许多工作,RFC3489中详细描述了。

 

TURN(Traveral Using Relay NAT)该方式是将所有的数据交换都经由服务器来完成,这样NAT将没有障碍,但服务器的负载、丢包、延迟性就是很大的问题。目前很多游戏均采用该方式避开NAT的问题。这种方式不叫p2p

 

ICE(Interactive Connectivity Establishment)是对上述各种技术的综合,但明显带来了复杂性。

 

总之,NAT的存在代表着一种时尚,那就是——不求简单,但求复杂,坚决把你搞晕,反正没我责任。


P2P 之 UDP穿透NAT的原理与实现(附源代码)

原创:shootingstars
参考:http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt

论坛上经常有对P2P原理的讨论,但是讨论归讨论,很少有实质的东西产生(源代码)。呵呵,在这里我就用自己实现的一个源代码来说明UDP穿越NAT的原理。

首先先介绍一些基本概念:
NAT(Network Address Translators),网络地址转换:网络地址转换是在IP地址日益缺乏的情况下产生的,它的主要目的就是为了能够地址重用。NAT分为两大类,基本的NAT和NAPT(Network Address/Port Translator)。
最开始NAT是运行在路由器上的一个功能模块。

最先提出的是基本的NAT,它的产生基于如下事实:一个私有网络(域)中的节点中只有很少的节点需要与外网连接(呵呵,这是在上世纪90年代中期提出的)。那么这个子网中其实只有少数的节点需要全球唯一的IP地址,其他的节点的IP地址应该是可以重用的。
因此,基本的NAT实现的功能很简单,在子网内使用一个保留的IP子网段,这些IP对外是不可见的。子网内只有少数一些IP地址可以对应到真正全球唯一的IP地址。如果这些节点需要访问外部网络,那么基本NAT就负责将这个节点的子网内IP转化为一个全球唯一的IP然后发送出去。(基本的NAT会改变IP包中的原IP地址,但是不会改变IP包中的端口)
关于基本的NAT可以参看RFC 1631

另外一种NAT叫做NAPT,从名称上我们也可以看得出,NAPT不但会改变经过这个NAT设备的IP数据报的IP地址,还会改变IP数据报的TCP/UDP端口。基本NAT的设备可能我们见的不多(呵呵,我没有见到过),NAPT才是我们真正讨论的主角。看下图:
Server S1
18.181.0.31:1235
|
^ Session 1 (A-S1) ^ |
| 18.181.0.31:1235 | |
v 155.99.25.11:62000 v |
|
NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ |
| 18.181.0.31:1235 | |
v 10.0.0.1:1234 v |
|
Client A
10.0.0.1:1234
有一个私有网络10.*.*.*,Client A是其中的一台计算机,这个网络的网关(一个NAT设备)的外网IP是155.99.25.11(应该还有一个内网的IP地址,比如10.0.0.10)。如果Client A中的某个进程(这个进程创建了一个UDP Socket,这个Socket绑定1234端口)想访问外网主机18.181.0.31的1235端口,那么当数据包通过NAT时会发生什么事情呢?
首先NAT会改变这个数据包的原IP地址,改为155.99.25.11。接着NAT会为这个传输创建一个Session(Session是一个抽象的概念,如果是TCP,也许Session是由一个SYN包开始,以一个FIN包结束。而UDP呢,以这个IP的这个端口的第一个UDP开始,结束呢,呵呵,也许是几分钟,也许是几小时,这要看具体的实现了)并且给这个Session分配一个端口,比如62000,然后改变这个数据包的源端口为62000。所以本来是(10.0.0.1:1234->18.181.0.31:1235)的数据包到了互联网上变为了(155.99.25.11:62000->18.181.0.31:1235)。
一旦NAT创建了一个Session后,NAT会记住62000端口对应的是10.0.0.1的1234端口,以后从18.181.0.31发送到62000端口的数据会被NAT自动的转发到10.0.0.1上。(注意:这里是说18.181.0.31发送到62000端口的数据会被转发,其他的IP发送到这个端口的数据将被NAT抛弃)这样Client A就与Server S1建立以了一个连接。

呵呵,上面的基础知识可能很多人都知道了,那么下面是关键的部分了。
看看下面的情况:
Server S1 Server S2
18.181.0.31:1235 138.76.29.7:1235
| |
| |
---------------------- ----------------------
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 155.99.25.11:62000 v | v 155.99.25.11:62000 v
|
Cone NAT
155.99.25.11
|
^ Session 1 (A-S1) ^ | ^ Session 2 (A-S2) ^
| 18.181.0.31:1235 | | | 138.76.29.7:1235 |
v 10.0.0.1:1234 v | v 10.0.0.1:1234 v
|
Client A
10.0.0.1:1234
接上面的例子,如果Client A的原来那个Socket(绑定了1234端口的那个UDP Socket)又接着向另外一个Server S2发送了一个UDP包,那么这个UDP包在通过NAT时会怎么样呢?
这时可能会有两种情况发生,一种是NAT再次创建一个Session,并且再次为这个Session分配一个端口号(比如:62001)。另外一种是NAT再次创建一个Session,但是不会新分配一个端口号,而是用原来分配的端口号62000。前一种NAT叫做Symmetric NAT,后一种叫做Cone NAT。我们期望我们的NAT是第二种,呵呵,如果你的NAT刚好是第一种,那么很可能会有很多P2P软件失灵。(可以庆幸的是,现在绝大多数的NAT属于后者,即Cone NAT)

好了,我们看到,通过NAT,子网内的计算机向外连结是很容易的(NAT相当于透明的,子网内的和外网的计算机不用知道NAT的情况)。
但是如果外部的计算机想访问子网内的计算机就比较困难了(而这正是P2P所需要的)。
那么我们如果想从外部发送一个数据报给内网的计算机有什么办法呢?首先,我们必须在内网的NAT上打上一个“洞”(也就是前面我们说的在NAT上建立一个Session),这个洞不能由外部来打,只能由内网内的主机来打。而且这个洞是有方向的,比如从内部某台主机(比如:192.168.0.10)向外部的某个IP(比如:219.237.60.1)发送一个UDP包,那么就在这个内网的NAT设备上打了一个方向为219.237.60.1的“洞”,(这就是称为UDP Hole Punching的技术)以后219.237.60.1就可以通过这个洞与内网的192.168.0.10联系了。(但是其他的IP不能利用这个洞)。

呵呵,现在该轮到我们的正题P2P了。有了上面的理论,实现两个内网的主机通讯就差最后一步了:那就是鸡生蛋还是蛋生鸡的问题了,两边都无法主动发出连接请求,谁也不知道谁的公网地址,那我们如何来打这个洞呢?我们需要一个中间人来联系这两个内网主机。
现在我们来看看一个P2P软件的流程,以下图为例:

Server S (219.237.60.1)
|
|
---------------------- ----------------------
| |
NAT A (外网IP:202.187.45.3) NAT B (外网IP:187.34.1.56)
| (内网IP:192.168.0.1) | (内网IP:192.168.0.1)
| |
Client A (192.168.0.20:4000) Client B (192.168.0.10:40000)

首先,Client A登录服务器,NAT A为这次的Session分配了一个端口60000,那么Server S收到的Client A的地址是202.187.45.3:60000,这就是Client A的外网地址了。同样,Client B登录Server S,NAT B给此次Session分配的端口是40000,那么Server S收到的B的地址是187.34.1.56:40000。
此时,Client A与Client B都可以与Server S通信了。如果Client A此时想直接发送信息给Client B,那么他可以从Server S那儿获得B的公网地址187.34.1.56:40000,是不是Client A向这个地址发送信息Client B就能收到了呢?答案是不行,因为如果这样发送信息,NAT B会将这个信息丢弃(因为这样的信息是不请自来的,为了安全,大多数NAT都会执行丢弃动作)。现在我们需要的是在NAT B上打一个方向为202.187.45.3(即Client A的外网地址)的洞,那么Client A发送到187.34.1.56:40000的信息,Client B就能收到了。这个打洞命令由谁来发呢,呵呵,当然是Server S。
总结一下这个过程:如果Client A想向Client B发送信息,那么Client A发送命令给Server S,请求Server S命令Client B向Client A方向打洞。呵呵,是不是很绕口,不过没关系,想一想就很清楚了,何况还有源代码呢(侯老师说过:在源代码面前没有秘密 8)),然后Client A就可以通过Client B的外网地址与Client B通信了。

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

下面是一个模拟P2P聊天的过程的源代码,过程很简单,P2PServer运行在一个拥有公网IP的计算机上,P2PClient运行在两个不同的NAT后(注意,如果两个客户端运行在一个NAT后,本程序很可能不能运行正常,这取决于你的NAT是否支持loopback translation,详见http://midcom-p2p.sourceforge.net/draft-ford-midcom-p2p-01.txt,当然,此问题可以通过双方先尝试连接对方的内网IP来解决,但是这个代码只是为了验证原理,并没有处理这些问题),后登录的计算机可以获得先登录计算机的用户名,后登录的计算机通过send username message的格式来发送消息。如果发送成功,说明你已取得了直接与对方连接的成功。
程序现在支持三个命令:send , getu , exit

send格式:send username message
功能:发送信息给username

getu格式:getu
功能:获得当前服务器用户列表

exit格式:exit
功能:注销与服务器的连接(服务器不会自动监测客户是否吊线)

代码很短,相信很容易懂,如果有什么问题,可以给我发邮件[email protected] 或者在CSDN上发送短消息。同时,欢迎转发此文,但希望保留作者版权8-)。

最后感谢CSDN网友 PiggyXP 和 Seilfer的测试帮助

P2PServer.c

/* P2P 程序服务端
*
* 文件名:P2PServer.c
*
* 日期:2004-5-21
*
* 作者:shootingstars([email protected])
*
*/
#pragma comment(lib, "ws2_32.lib")

#include "windows.h"
#include "../proto.h"
#include "../Exception.h"

UserList ClientList;

void InitWinSock()
{
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Windows sockets 2.2 startup");
throw Exception("");
}
else{
printf("Using %s (Status: %s)/n",
wsaData.szDescription, wsaData.szSystemStatus);
printf("with API versions %d.%d to %d.%d/n/n",
LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));

}
}

SOCKET mksock(int type)
{
SOCKET sock = socket(AF_INET, type, 0);
if (sock < 0)
{
printf("create socket error");
throw Exception("");
}
return sock;
}

stUserListNode GetUser(char *username)
{
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
UserIterator)
{
if( strcmp( ((*UserIterator)->userName), username) == 0 )
return *(*UserIterator);
}
throw Exception("not find this user");
}

int main(int argc, char* argv[])
{
try{
InitWinSock();

SOCKET PrimaryUDP;
PrimaryUDP = mksock(SOCK_DGRAM);

sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port= htons(SERVER_PORT);
local.sin_addr.s_addr = htonl(INADDR_ANY);
int nResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));
if(nResult==SOCKET_ERROR)
throw Exception("bind error");

sockaddr_in sender;
stMessage recvbuf;
memset(&recvbuf,0,sizeof(stMessage));

// 开始主循环.
// 主循环负责下面几件事情:
// 一:读取客户端登陆和登出消息,记录客户列表
// 二:转发客户p2p请求
for(;;)
{
int dwSender = sizeof(sender);
int ret = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(stMessage), 0, (sockaddr *)&sender, &dwSender);
if(ret <= 0)
{
printf("recv error");
continue;
}
else
{
int messageType = recvbuf.iMessageType;
switch(messageType){
case LOGIN:
{
// 将这个用户的信息记录到用户列表中
printf("has a user login : %s/n", recvbuf.message.loginmember.userName);
stUserListNode *currentuser = new stUserListNode();
strcpy(currentuser->userName, recvbuf.message.loginmember.userName);
currentuser->ip = ntohl(sender.sin_addr.S_un.S_addr);
currentuser->port = ntohs(sender.sin_port);

ClientList.push_back(currentuser);

// 发送已经登陆的客户信息
int nodecount = (int)ClientList.size();
sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
UserIterator)
{
sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender));
}

break;
}
case LOGOUT:
{
// 将此客户信息删除
printf("has a user logout : %s/n", recvbuf.message.logoutmember.userName);
UserList::iterator removeiterator = NULL;
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
UserIterator)
{
if( strcmp( ((*UserIterator)->userName), recvbuf.message.logoutmember.userName) == 0 )
{
removeiterator = UserIterator;
break;
}
}
if(removeiterator != NULL)
ClientList.remove(*removeiterator);
break;
}
case P2PTRANS:
{
// 某个客户希望服务端向另外一个客户发送一个打洞消息
printf("%s wants to p2p %s/n",inet_ntoa(sender.sin_addr),recvbuf.message.translatemessage.userName);
stUserListNode node = GetUser(recvbuf.message.translatemessage.userName);
sockaddr_in remote;
remote.sin_family=AF_INET;
remote.sin_port= htons(node.port);
remote.sin_addr.s_addr = htonl(node.ip);

in_addr tmp;
tmp.S_un.S_addr = htonl(node.ip);
printf("the address is %s,and port is %d/n",inet_ntoa(tmp), node.port);

stP2PMessage transMessage;
transMessage.iMessageType = P2PSOMEONEWANTTOCALLYOU;
transMessage.iStringLen = ntohl(sender.sin_addr.S_un.S_addr);
transMessage.Port = ntohs(sender.sin_port);

sendto(PrimaryUDP,(const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr *)&remote, sizeof(remote));

break;
}

case GETALLUSER:
{
int command = GETALLUSER;
sendto(PrimaryUDP, (const char*)&command, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

int nodecount = (int)ClientList.size();
sendto(PrimaryUDP, (const char*)&nodecount, sizeof(int), 0, (const sockaddr*)&sender, sizeof(sender));

for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
UserIterator)
{
sendto(PrimaryUDP, (const char*)(*UserIterator), sizeof(stUserListNode), 0, (const sockaddr*)&sender, sizeof(sender));
}
break;
}
}
}
}

}
catch(Exception &e)
{
printf(e.GetMessage());
return 1;
}

return 0;
}

/* P2P 程序客户端
*
* 文件名:P2PClient.c
*
* 日期:2004-5-21
*
* 作者:shootingstars([email protected])
*
*/

#pragma comment(lib,"ws2_32.lib")

#include "windows.h"
#include "../proto.h"
#include "../Exception.h"
#include <iostream>
using namespace std;

UserList ClientList;

 

#define COMMANDMAXC 256
#define MAXRETRY 5

SOCKET PrimaryUDP;
char UserName[10];
char ServerIP[20];

bool RecvedACK;

void InitWinSock()
{
WSADATA wsaData;

if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0)
{
printf("Windows sockets 2.2 startup");
throw Exception("");
}
else{
printf("Using %s (Status: %s)/n",
wsaData.szDescription, wsaData.szSystemStatus);
printf("with API versions %d.%d to %d.%d/n/n",
LOBYTE(wsaData.wVersion), HIBYTE(wsaData.wVersion),
LOBYTE(wsaData.wHighVersion), HIBYTE(wsaData.wHighVersion));
}
}

SOCKET mksock(int type)
{
SOCKET sock = socket(AF_INET, type, 0);
if (sock < 0)
{
printf("create socket error");
throw Exception("");
}
return sock;
}

stUserListNode GetUser(char *username)
{
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
UserIterator)
{
if( strcmp( ((*UserIterator)->userName), username) == 0 )
return *(*UserIterator);
}
throw Exception("not find this user");
}

void BindSock(SOCKET sock)
{
sockaddr_in sin;
sin.sin_addr.S_un.S_addr = INADDR_ANY;
sin.sin_family = AF_INET;
sin.sin_port = 0;

if (bind(sock, (struct sockaddr*)&sin, sizeof(sin)) < 0)
throw Exception("bind error");
}

void ConnectToServer(SOCKET sock,char *username, char *serverip)
{
sockaddr_in remote;
remote.sin_addr.S_un.S_addr = inet_addr(serverip);
remote.sin_family = AF_INET;
remote.sin_port = htons(SERVER_PORT);

stMessage sendbuf;
sendbuf.iMessageType = LOGIN;
strncpy(sendbuf.message.loginmember.userName, username, 10);

sendto(sock, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote,sizeof(remote));

int usercount;
int fromlen = sizeof(remote);
int iread = recvfrom(sock, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
if(iread<=0)
{
throw Exception("Login error/n");
}

// 登录到服务端后,接收服务端发来的已经登录的用户的信息
cout<<"Have "<<usercount<<" users logined server:"<<endl;
for(int i = 0;i<usercount;i )
{
stUserListNode *node = new stUserListNode;
recvfrom(sock, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
ClientList.push_back(node);
cout<<"Username:"<<node->userName<<endl;
in_addr tmp;
tmp.S_un.S_addr = htonl(node->ip);
cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;
cout<<"UserPort:"<<node->port<<endl;
cout<<""<<endl;
}
}

void OutputUsage()
{
cout<<"You can input you command:/n"
<<"Command Type:/"send/",/"exit/",/"getu/"/n"
<<"Example : send Username Message/n"
<<" exit/n"
<<" getu/n"
<<endl;
}

/* 这是主要的函数:发送一个消息给某个用户(C)
*流程:直接向某个用户的外网IP发送消息,如果此前没有联系过
* 那么此消息将无法发送,发送端等待超时。
* 超时后,发送端将发送一个请求信息到服务端,
* 要求服务端发送给客户C一个请求,请求C给本机发送打洞消息
* 以上流程将重复MAXRETRY次
*/
bool SendMessageTo(char *UserName, char *Message)
{
char realmessage[256];
unsigned int UserIP;
unsigned short UserPort;
bool FindUser = false;
for(UserList::iterator UserIterator=ClientList.begin();
UserIterator!=ClientList.end();
UserIterator)
{
if( strcmp( ((*UserIterator)->userName), UserName) == 0 )
{
UserIP = (*UserIterator)->ip;
UserPort = (*UserIterator)->port;
FindUser = true;
}
}

if(!FindUser)
return false;

strcpy(realmessage, Message);
for(int i=0;i<MAXRETRY;i )
{
RecvedACK = false;

sockaddr_in remote;
remote.sin_addr.S_un.S_addr = htonl(UserIP);
remote.sin_family = AF_INET;
remote.sin_port = htons(UserPort);
stP2PMessage MessageHead;
MessageHead.iMessageType = P2PMESSAGE;
MessageHead.iStringLen = (int)strlen(realmessage) 1;
int isend = sendto(PrimaryUDP, (const char *)&MessageHead, sizeof(MessageHead), 0, (const sockaddr*)&remote, sizeof(remote));
isend = sendto(PrimaryUDP, (const char *)&realmessage, MessageHead.iStringLen, 0, (const sockaddr*)&remote, sizeof(remote));

// 等待接收线程将此标记修改
for(int j=0;j<10;j )
{
if(RecvedACK)
return true;
else
Sleep(300);
}

// 没有接收到目标主机的回应,认为目标主机的端口映射没有
// 打开,那么发送请求信息给服务器,要服务器告诉目标主机
// 打开映射端口(UDP打洞)
sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);

stMessage transMessage;
transMessage.iMessageType = P2PTRANS;
strcpy(transMessage.message.translatemessage.userName, UserName);

sendto(PrimaryUDP, (const char*)&transMessage, sizeof(transMessage), 0, (const sockaddr*)&server, sizeof(server));
Sleep(100);// 等待对方先发送信息。
}
return false;
}

// 解析命令,暂时只有exit和send命令
// 新增getu命令,获取当前服务器的所有用户
void ParseCommand(char * CommandLine)
{
if(strlen(CommandLine)<4)
return;
char Command[10];
strncpy(Command, CommandLine, 4);
Command[4]='/0';

if(strcmp(Command,"exit")==0)
{
stMessage sendbuf;
sendbuf.iMessageType = LOGOUT;
strncpy(sendbuf.message.logoutmember.userName, UserName, 10);
sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);

sendto(PrimaryUDP,(const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr *)&server, sizeof(server));
shutdown(PrimaryUDP, 2);
closesocket(PrimaryUDP);
exit(0);
}
else if(strcmp(Command,"send")==0)
{
char sendname[20];
char message[COMMANDMAXC];
int i;
for(i=5;;i )
{
if(CommandLine[i]!=' ')
sendname[i-5]=CommandLine[i];
else
{
sendname[i-5]='/0';
break;
}
}
strcpy(message, &(CommandLine[i 1]));
if(SendMessageTo(sendname, message))
printf("Send OK!/n");
else
printf("Send Failure!/n");
}
else if(strcmp(Command,"getu")==0)
{
int command = GETALLUSER;
sockaddr_in server;
server.sin_addr.S_un.S_addr = inet_addr(ServerIP);
server.sin_family = AF_INET;
server.sin_port = htons(SERVER_PORT);

sendto(PrimaryUDP,(const char*)&command, sizeof(command), 0, (const sockaddr *)&server, sizeof(server));
}
}

// 接受消息线程
DWORD WINAPI RecvThreadProc(LPVOID lpParameter)
{
sockaddr_in remote;
int sinlen = sizeof(remote);
stP2PMessage recvbuf;
for(;;)
{
int iread = recvfrom(PrimaryUDP, (char *)&recvbuf, sizeof(recvbuf), 0, (sockaddr *)&remote, &sinlen);
if(iread<=0)
{
printf("recv error/n");
continue;
}
switch(recvbuf.iMessageType)
{
case P2PMESSAGE:
{
// 接收到P2P的消息
char *comemessage= new char[recvbuf.iStringLen];
int iread1 = recvfrom(PrimaryUDP, comemessage, 256, 0, (sockaddr *)&remote, &sinlen);
comemessage[iread1-1] = '/0';
if(iread1<=0)
throw Exception("Recv Message Error/n");
else
{
printf("Recv a Message:%s/n",comemessage);

stP2PMessage sendbuf;
sendbuf.iMessageType = P2PMESSAGEACK;
sendto(PrimaryUDP, (const char*)&sendbuf, sizeof(sendbuf), 0, (const sockaddr*)&remote, sizeof(remote));
}

delete []comemessage;
break;

}
case P2PSOMEONEWANTTOCALLYOU:
{
// 接收到打洞命令,向指定的IP地址打洞
printf("Recv p2someonewanttocallyou data/n");
sockaddr_in remote;
remote.sin_addr.S_un.S_addr = htonl(recvbuf.iStringLen);
remote.sin_family = AF_INET;
remote.sin_port = htons(recvbuf.Port);

// UDP hole punching
stP2PMessage message;
message.iMessageType = P2PTRASH;
sendto(PrimaryUDP, (const char *)&message, sizeof(message), 0, (const sockaddr*)&remote, sizeof(remote));

break;
}
case P2PMESSAGEACK:
{
// 发送消息的应答
RecvedACK = true;
break;
}
case P2PTRASH:
{
// 对方发送的打洞消息,忽略掉。
//do nothing ...
printf("Recv p2ptrash data/n");
break;
}
case GETALLUSER:
{
int usercount;
int fromlen = sizeof(remote);
int iread = recvfrom(PrimaryUDP, (char *)&usercount, sizeof(int), 0, (sockaddr *)&remote, &fromlen);
if(iread<=0)
{
throw Exception("Login error/n");
}

ClientList.clear();

cout<<"Have "<<usercount<<" users logined server:"<<endl;
for(int i = 0;i<usercount;i )
{
stUserListNode *node = new stUserListNode;
recvfrom(PrimaryUDP, (char*)node, sizeof(stUserListNode), 0, (sockaddr *)&remote, &fromlen);
ClientList.push_back(node);
cout<<"Username:"<<node->userName<<endl;
in_addr tmp;
tmp.S_un.S_addr = htonl(node->ip);
cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;
cout<<"UserPort:"<<node->port<<endl;
cout<<""<<endl;
}
break;
}
}
}
}


int main(int argc, char* argv[])
{
try
{
InitWinSock();

PrimaryUDP = mksock(SOCK_DGRAM);
BindSock(PrimaryUDP);

cout<<"Please input server ip:";
cin>>ServerIP;

cout<<"Please input your name:";
cin>>UserName;

ConnectToServer(PrimaryUDP, UserName, ServerIP);

HANDLE threadhandle = CreateThread(NULL, 0, RecvThreadProc, NULL, NULL, NULL);
CloseHandle(threadhandle);
OutputUsage();

for(;;)
{
char Command[COMMANDMAXC];
gets(Command);
ParseCommand(Command);
}
}
catch(Exception &e)
{
printf(e.GetMessage());
return 1;
}
return 0;
}

/* 异常类
*
* 文件名:Exception.h
*
* 日期:2004.5.5
*
* 作者:shootingstars([email protected])
*/

#ifndef __HZH_Exception__
#define __HZH_Exception__

#define EXCEPTION_MESSAGE_MAXLEN 256
#include "string.h"

class Exception
{
private:
char m_ExceptionMessage[EXCEPTION_MESSAGE_MAXLEN];
public:
Exception(char *msg)
{
strncpy(m_ExceptionMessage, msg, EXCEPTION_MESSAGE_MAXLEN);
}

char *GetMessage()
{
return m_ExceptionMessage;
}
};

#endif

/* P2P 程序传输协议
*
* 日期:2004-5-21
*
* 作者:shootingstars([email protected])
*
*/

#pragma once
#include <list>

// 定义iMessageType的值
#define LOGIN 1
#define LOGOUT 2
#define P2PTRANS 3
#define GETALLUSER 4

// 服务器端口
#define SERVER_PORT 2280

// Client登录时向服务器发送的消息
struct stLoginMessage
{
char userName[10];
char password[10];
};

// Client注销时发送的消息
struct stLogoutMessage
{
char userName[10];
};

// Client向服务器请求另外一个Client(userName)向自己方向发送UDP打洞消息
struct stP2PTranslate
{
char userName[10];
};

// Client向服务器发送的消息格式
struct stMessage
{
int iMessageType;
union _message
{
stLoginMessage loginmember;
stLogoutMessage logoutmember;
stP2PTranslate translatemessage;
}message;
};

// 客户节点信息
struct stUserListNode
{
char userName[10];
unsigned int ip;
unsigned short port;
};

// Server向Client发送的消息
struct stServerToClient
{
int iMessageType;
union _message
{
stUserListNode user;
}message;

};

//======================================
// 下面的协议用于客户端之间的通信
//======================================
#define P2PMESSAGE 100 // 发送消息
#define P2PMESSAGEACK 101 // 收到消息的应答
#define P2PSOMEONEWANTTOCALLYOU 102 // 服务器向客户端发送的消息
// 希望此客户端发送一个UDP打洞包
#define P2PTRASH 103 // 客户端发送的打洞包,接收端应该忽略此消息

// 客户端之间发送消息格式
struct stP2PMessage
{
int iMessageType;
int iStringLen; // or IP address
unsigned short Port;
};

using namespace std;
typedef list<stUserListNode *> UserList;

 

工程下载地址:upload/2004_05/04052509317298.rar


你可能感兴趣的:(P2P之UDP穿透NAT的原理与实现 & Skype(已整理过))