以前大家讨论的内网P2P通讯模式:
P1-->NAT-->S-->NAT-->P2 
可以先P1,P2都向S发登陆信息,S记录P1,P2的在各自NAT里的断口映射,然后P1,P2就可以获取彼此的断口映射,实现PP通讯了我现在设想一种通讯模式:
P1-->NAT-->S(web.如:SQL Server)-->NAT-->P2 
 具体通讯如下:
1、P1尝试发送信息到外网(任意有效IP),然后在NAT里获取自己的断口映射,P2也如此
2、P1把获得的影射发送的SQL Server 保存,P2也如此
3、P1获取通过访问SQL SERVER获取P2的映射,p2也如此
4、P1通过P2的映射发送信息给P2如果第一步能实现,下面的肯定都能实现,下面问题来了,关键是在第一步中“P1尝试发送信息到外网(任意有效IP)”时,如何“在NAT里获取自己的断口映射”?
不知道有没有人这样做过,欢迎大家讨论P2P的各种实现方法。

解决方案 »

  1.   

    作者: shootingstars (有容乃大,无欲则刚) | 日期: 2004-05-25 | 已阅: 23382次 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的端口是顺序分配的,那么我们或许可以猜测这个端口号,可是由于可能导致失败的因素太多,我们不推荐这种猜测端口的方法)。
      

  2.   

    按楼上的意思已就是说内网自己主机无法获得NAT为自己分配了哪个端口?
      

  3.   

    那篇文章已经很清楚了,如果nat为Cone NAT你的想法就是可以实现的,不过你的想法的前提就是每次内网的电脑被分配的端口是固定的,但是这个好象不能保证阿,所以必须每次都要更新你的数据库,还不如不放入数据库了。
      

  4.   

    只有在支持并开启UPnP的路由才能不通过第三方获取自己的端口。
      

  5.   

    mathsword(梦在流浪):
    你的意思内网主机用同一个线程同一个端口向外发送信息的时候,在NAT建立的端口映射会变化?
    如果是这样的话,外网接收到发出去的信息,再发回去内网的主机不是收不到了(因为照你的意思说端口映射已经改变了)
      

  6.   

    tuoshi(小才-升星开心!) :
    在支持并开启UPnP的路由如何获取,请指教,谢谢!
      

  7.   

    (1) SERVER S启动
    (2) CLIENT A启动,连接SERVER S。SERVER S记录CLIENT A 的公网IPA,PORTA。
    (3) CLIENT B启动,连接SERVER S。SERVER S记录CLIENT B 的公网IPB,PORTB。 
    (4) CLIENT A问讯SERVER S得到CLIENT B 的公网IPB,PORTB。
    (5) CLIENT B问讯SERVER S得到CLIENT A 的公网IPA,PORTA。
    (6) 为了简单起见,假设有个宇宙绝对时钟,并且CLIENT A,CLIENT B的时钟绝对完全保持一样。
    (7) 假设在某个时刻TIME_a1 ,CLIENT A向 CLIENT B (即地址IPB,PORTB)发送一个UDP包PACKET_A。
    (8) 在时刻TIME_a2,UDP包PACKET_A到了CLIENT B所在的NAT设备上(注意这时侯的时刻是TIME_a2); 
    (9) 假设在某个时刻TIME_b1 ,CLIENT B向 CLIENT A (即地址IPA,PORTA)发送一个UDP包PACKET_B。
    (10) 在时刻TIME_b2,UDP包PACKET_B到了CLIENT A所在的NAT设备上(注意这时侯的时刻是TIME_b2);
    11) 好了,现在来分析TIME_a1,TIME_a2,TIME_b1,TIME_b2的关系。 
    (12) 在时刻TIME_a1,CLIENT A给CLIENT B打了一个洞。那么如果TIME_a1小于TIME_b2的话,因为这时CLIENT A已经为CLIENT B打开了洞,那么UDP包PACKET_B就能到达CLIENT A了。反之如果TIME_a1大于TIME_b2,因为还没打开洞,那么CLIENT A 的NAT设备就丢弃CLIENT B发来的UDP包PACKET_B。 
    (13) 同理在时刻TIME_b1,CLIENT B给CLIENT A打了一个洞。那么如果TIME_b1小于TIME_a2的话,因为这时CLIENT B已经为CLIENT A打开了洞,那么UDP包PACKET_A就能到达CLIENT B了。反之如果TIME_b1大于TIME_a2,因为还没打开洞,那么CLIENT B 的NAT设备就丢弃CLIENT A发来的UDP包PACKET_A。 
    14) 如果上面(12),(13)只要有一方能到达,就能建立直接访问了。如果都没到达的话,那也没关系,不停的重复7~13过程,就必定能够到达满足条件。就能达到两个内网互相通讯的目的了。(除非等你等得花儿也谢了!:))
    (15)结束 
      

  8.   

    3.4. UDP port number prediction UPD端口号预言A variant of the UDP hole punching technique discussed above exists that allows P2P UDP sessions to be created in the presence of some symmetric NATs.  This method is sometimes called the "N+1" technique [BIDIR] and is explored in detail by Takeda [SYM-STUN]. The method works by analyzing the behavior of the NAT and attempting to predict the public port numbers it will assign to future sessions.   Consider again the situation in which two clients, A and B, each behind a separate NAT, have each established UDP connections with a permanently addressable server S:   让我们来考虑这样一种情况,有两个客户端 A 和 B,他们都藏在不同的NAT后面,他们都开放了一个UDP连接给具有固定IP的Server S:如下图    NAT A has assigned its own UDP port 62000 to the communication session between A and S, and NAT B has assigned its port 31000 to the session between B and S.  By communicating through server S, A and B learn each other's public IP addresses and port numbers as observed   by S.  Client A now starts sending UDP messages to port 31001 at address 138.76.29.7 (note the port number increment), and client B simultaneously starts sending messages to port 62001 at address 155.99.25.11.  If NATs A and B assign port numbers to new sessions  sequentially, and if not much time has passed since the A-S and B-S sessions were initiated, then a working bi-directional communication channel between A and B should result.     A's messages to B cause NAT A  to open up a new session, to which NAT A will (hopefully) assign public port number 62001, because 62001 is next in sequence after the  port number 62000 it previously assigned to the session between A and S.  Similarly, B's messages to A will cause NAT B to open a new   session, to which it will (hopefully) assign port number 31001.  If both clients have correctly guessed the port numbers each NAT assigns to the new sessions, then a bi-directional UDP communication channel will have been established as shown below.    NAT A 分配了它自己的UDP端口62000,用来保持 客户端A 与 服务器S 的通信会话, NAT B 也分配了31000端口,用来保持 客户端B 与 服务器S 的通信会话。通过与 服务器S的对话,客户端A 和 客户端B 都相互知道了对方所映射的真实IP和端口。   客户端A发送一条UDP消息到 138.76.29.7:31001(请注意到端口号的增加),同时 客户端B发送一条UDP消息到 155.99.25.11:62001。如果NAT A 和NAT B继续分配端口给新的会话,并且从A-S和B-S的会话时间消耗得并不多的话,那么一条处于客户端A和客户端B之间的双向会话通道就建立了。   客户端A发出的消息送达B导致了NAT A打开了一个新的会话,并且我们希望 NAT A将会指派62001端口给这个新的会话,因为62001是继62000后,NAT会自动指派给 从服务器S到客户端A之间的新会话的端口号;类似的,客户端B发出的消息送达A导致了 NAT B打开了一个新的会话,并且我们希望 NAT B 将会指派31001这个端口给新的会话;如果两个客户端都正确的猜测到了对方新会话被指派的端口号,那么这个 客户端A-客户端B的双向连接就被打通了。其结果如下图所示:   Obviously there are many things that can cause this trick to fail. If the predicted port number at either NAT already happens to be in use by an unrelated session, then the NAT will skip over that port number and the connection attempt will fail.  If either NAT sometimes or always chooses port numbers non-sequentially, then the trick will fail.  
       
       If a different client behind NAT A (or B respectively) opens up a new outgoing UDP connection to any external destination after A (B) establishes its connection with S but before sending its first message to B (A), then the unrelated client will inadvertently "steal" the desired port number.  This trick is therefore much less likely to work when either NAT involved is under load.  明显的,有许多因素会导致这个方法失败:如果这个重定向的新端口(62001和31001) 恰好已经被一个不想关的会话所使用,那么NAT就会跳过这个端口号,这个连接就会宣告 失败;如果两个NAT有时或者总是不按照顺序来生成新的端口号,那么这个方法也是行不通的。   如果隐藏在NAT A后的一个不同的客户端X(或者在NAT B后)打开了一个新的“外出”UDP 连接,并且无论这个连接的目的如何;只要这个动作发生在 客户端A 建立了与服务器S 的连接之后,客户端A 与 客户端B 建立连接之前,那么这个无关的客户端X 就会趁人不备地“偷” 到这个被渴望分配的端口。所以,这个方法变得如此脆弱而且不堪一击,只要任何一个NAT方包含以上碰到的问题,这个方法都不会奏效。      
       Since in practice a P2P application implementing this trick would still need to work if the NATs are cone NATs, or if one is a cone NAT and the other is a symmetric NAT, the application would need to detect beforehand what kind of NAT is involved on either end [STUN] and modify its behavior accordingly, increasing the complexity of the algorithm and the general brittleness of the network.      Finally, port number prediction has no chance of working if either client is behind two or more levels of NAT and the NAT(s) closest to the client are symmetric.  For all of these reasons, it is NOT recommended that new applications implement this trick; it is mentioned here for historical and informational purposes.    自从使用这种方法来实践P2P的应用程序以来,在处于 cone NAT 系列的网络环境中这个方法还是实用的;如果有一方为 cone NAT 而另外一方为 symmetric NAT,那么应用程序就应该预先发现另外一方的 NAT 是什么类型,再做出正确的行为来处理通信,这样就增大了算法的复杂度,并且降低了在真实网络环境中的普适性。    最后,如果P2P的一方处在两级或者两级以上的NAT下面,并且这些NATs 接近这个客户端是 symmetric的话,端口号预言 是无效的!    因此,并不推荐使用这个方法来写新的P2P应用程序,这也是历史的经验和教训!