• 不同局域网内经Internet的P2P通信技术


    文章来源:SNS 社区

     

    以下将要用到一个叫做NAT的重要名词,先做点解释。

     

    NATNet Address Translation(网络地址转换)的简称,就是说,局域网通常靠一个具有公网IP的代理网关服务器连到Internet共享上网。局域网内的机器并不具备公网IP地址,它只有内网地址,假设它要和Internet上的HTTP服务器通信,代理网关便会新建一个端口来和这个网内机器关联,并通过这个端口来和HTTP服务器交换数据。最终,网内机器->代理网关->HTTP服务器,在一个会话期间,各自的端口保持了映射关系,特别是代理网关和网内机器的端口映射,使得代理网关不会把接收到的数据向网内转发时,发错了机器。

     

    局域网内的机器在网关处,就是靠NAT来映射端口并实现Internet连接,因此,NAT也直接被称为“端口映射”。端口映射之后,在一个会话期间保持,对于TCP连接是直到连接断开才销毁,而对于UDP,却存在一个不定的生存期,例如2秒。

     

    如果两台机器AB,分别处于两个局域网内,它们要通过Internet通信,这就是P2P(点到点)连接通信。

     

    目前的Internet使用IPv4协议,采用32IP地址,主要被用来进行C/S形式的通信,需要共享的资源集中放于Internet服务器上。IPv4对于P2P分布式资源共享的支持,极不友好。首先,32IP地址已经不敷使用,公网IP地址日趋紧张,只能使用局域网共享公网IP的方式,局域网正是为了临时应对IP耗尽而出现的,长远的解决办法是研究IPv6。其次,分别处于两个局域网内的机器要通信,由于对方没有公网IP,直接呼叫对方是不可能的,必须借助第三方“中介”(机器或者软件)间接地连通,解决办法下列几种:

     

    第一:实现局域网内的数据链路层协议,就是写一个类似于TCP/IP的协议,由它来代替Windows系统里的TCP/IP协议,由它直接基于网卡硬件获取数据。这是十分复杂的。

     

    第二:用Internet上的公网服务器中转数据,但对于大数据量的中转,显然受到服务器和网络的负载极限的限制。

     

    第三:依靠Internet上的公网服务器做“媒人”,将这两台分别处于不同局域网的机器相互介绍给对方,在它们建立连接之后,服务器即脱离关系。这种方式下,服务器把ANAT端口映射关系告诉B,又把BNAT端口映射关系告诉A,这样AB相互知道对方的端口映射关系之后,就能建立连接。因为AB各自的端口映射关系是靠各自的代理网关动态建立的,动态建立的映射端口不得不告知对方。

     

    第四:上面的第三种办法,也可以采用静态端口映射方式,这样就不需要中介服务器对AB做介绍。在各方的代理网关上,可以在代理工具里将某个端口(1350)和局域网内的某台机器(如内网IP200.200.200.100,端口1360)做好静态映射,这样,代理网关会自动地将出入于1350端口的数据发往200.200.200.1001360端口。当然,通信之前,必须对对方的端口映射关系做配置。有多少台网内机器要通信,就得映射多少个不同的端口,同时在另一个局域网内的机器就要做多少个配置。在局域网内搭建HTTPFTP等服务器就是通过静态映射端口来实现的,这个端口一般不是HTTPFTP的默认8023,所以对这类站点的访问往往会在URL里加上端口号。

     

    由此可见,上述前两种办法在简单应用中是不可取的,只有后两种可行。它们又各有缺点,第三种动态映射端口,需要增加中间服务器,第四种静态映射端口,在需要通信的各方机器很多的情况下,做手工端口映射和配置都是很繁琐的,并且一方添加一台机器,就需要在其余对方增加配置。

     

    采用动态和静态相结合的办法是可以推想的,然而其可行性还必须经过测试。可以这样设计,为了让所有通信机器彼此知晓并定位。我们可以在局域网里,只对一台机器在代理网关处做静态端口映射,本局域网内的机器都向它登记。而两个局域网各自只做一项对对方的映射配置。两个局域网之间,没有静态映射端口的机器要通信,就靠有映射的机器来担当“介绍”。

     

    就局域网和NAT的问题实际上还很多,比如各自的局域网的结构不同,局域网里可能又有子局域网,局域网可能是NAT代理结构,但也可能是HTTP代理,Sock4Sock5代理等结构,NAT又分严格的和非严格NAT,严格NAT限制很多,更不便于P2P。不过,软件不能实现的地方,可以考虑改变硬件结构,例如将严格NAT变为非严格NAT。如果硬件改变不得,那么Internet整体上就有10%的系统不能实现P2P,除非等到正处于研发的IPv6协议出来。

     

    P2P要解决的唯一技术难题是如何发现、定位和寻址对方,就是如何穿透NATHTTPSock等代理和如何穿透防火墙找到对方并建立起通信的问题。由于绝大多数局域网是NAT代理结构,所以前面对NAT论述比较详细,也是网上讨论最多的话题,相比之下,穿透HttpSock代理就简单一些。此外,穿透NAT发现对等点的办法还有一些,例如多播,但由于现有Internet对多播并不友好,同时多播是无连接和不可靠的,其实现有难度。

     

    许多软件都是按照上述一些技术实现了P2P通信,著名的有MSNQQBitTorrent下载软件等。

     

    实际上,围绕P2P通信,尤其是两个不同局域网间的P2P,已经有许多的P2P协议和开发包涌现。例如,Sun公司以Java写的开发包Jxta,微软在Windows XP平台上有P2P的β版开发包,Intel公布.Net平台上的P2P应用开发工具包,放到微软有关.Net平台的新闻站点www.gotdotnet.com上供用户免费下载。

     

    但是利用它们来开发程序,非常繁琐,我们需要用简单的实现完成功能就可以了。

    如果想研究得更深入仔细,请从Sun公司的网站和微软网站下载开发包,或者在Google

    搜索协议和开发包。

     

    下面其实有两个实例,讲述连通的过程,包括简单伪代码。

    我们不希望在IP层实现我们的P2P,而是希望在应用层,利用Windows提供的Socket建立P2P,至多下到用原始Raw Socket来写P2P

    首先看,我们对于公网有服务器做“中介(非中转)”的P2P怎么实现。

     

    原理讲述:

    例如AB两台机器分别处于两个不同的局域网后,由Server做中介,先看连接过程。

    A首先连接服务器,采用UDP发包给Server,这个包包括了A的用户信息,类似于QQ

    QQ号、呢称等。Server方,可以用CSocket::GetPeerName()得到AIP及端口,但得到的IP和端口应该是A的代理网关的公网PublicIP及其映射端口NatPort,该映射端口就是A的代理网关为A的本次UDP通信临时分配的Nat端口。可以断言,得到的端口一定不是A的内网IP和内网UDP端口。

    服务器然后将A的公网IP、映射端口、用户信息等保存到(内存列表或者数据库),这样标志着A已经上线。服务器马上将其它在线的用户信息发回给A,包括其它用户的代理网关的公网IPNat端口。A同样将在线用户的这些信息保存并显示为列表,期待A用户做出选择。

    对于B,同样有上述的上线过程。

    A用户做出选择,要和在线的B用户通信时,A首先发UDP包给B的公网IPNat端口,并立即发一个UDP包给服务器,让服务器去通知B,叫BA也发一个UDP包。

    换句话说,1A发包给PublicB, 2A发包给Server3Server发包给PublicB4B发包给PublicA

    上面的叙述用到了"Public"字样,它代表代理网关的公网IP及其映射端口。

     

    由于AB各自的网关都保存了各自的端口映射关系,发到网关的数据,网关会按照这个映射关系转发给AB

    AB都分别收到对方发来的UDP包以后,连接宣告成功,服务器即可以脱离,AB即可以用UDP通信。

     

    何以如此麻烦?

     

    A在发UDP包给Server上线时,A的网关(A.Gate)就分配一个Nat端口(A.NatPort)A,用于AServer间的本次UDP会话,但A的网关明确标记,这个Nat端口,仅能用于AServer之间的UDP通信,不能挪着它用。并且,这个临时分配的端口,只能保持一个很短的时效,也许是一两秒吧。这个时间内,如果AServer没有任何通信,那么这个映射端口就宣告无效。下次,AServer又要通信时,A的网关又会重新分配一个新的端口。这段表明三点:

     

    1AServer的通信,需要A网关分配Nat端口来中转。

    2Nat端口只能用于AServer间的通信。

    3Nat端口存在生存期,长时间AServer无通信,该端口即宣告无效。

    就是这些麻烦,使得我们的连接过程必须绕很多弯。

     

    AB的通信,就是借助事先AB分别与Server连接时,在各自的网关处建立的端口映射来通信。为避免上面的23点麻烦,AB在初次连接时,必须几乎同时向对方发包。

    如果AB不同时发包给对方,它们各自的网关就会虑掉对方的包,因为该包不是Server发来的包,叫做不请自来的包。

     

    并且,即便AB各自的网关不虑掉非Server发来的包,它们各自的Nat端口也有一个时效。那么AServerBServer就不得不发心跳包,以维持各自的映射端口,保证其不失效。

     

    上面的过程中,如果AB建立连接失败,可以循环这个过程,直到一个有限的次数之后,仍不能连接则宣告失败。本文一部分的原文如下:

     

     

    Clients Behind Different NATs

     

      Suppose clients A and B both have private IP addresses and lie behind

     

      different network address translators. The peer-to-peer application

     

      running on clients A and B and on server S each use UDP port 1234. A

     

      and B have each initiated UDP communication sessions with server S,

     

      causing NAT A to assign its own public UDP port 62000 for A's session

     

      with S, and causing NAT B to assign its port 31000 to B's session

     

      with S, respectively.

     

     

                        Server S

     

                      18.181.0.31:1234

     

                          |

     

                          |

     

            +----------------------+----------------------+

     

            |                               |

     

          NAT A                           NAT B

     

      155.99.25.11:62000                   138.76.29.7:31000

     

            |                               |

     

            |                               |

     

          Client A                         Client B

     

        10.0.0.1:1234                       10.1.1.3:1234

     

     

      Now suppose that client A wants to establish a UDP communication

     

      session directly with client B. If A simply starts sending UDP

     

      requests to B's public address, 138.76.29.7:31000, then NAT B will

     

      typically discard these incoming messages because the source address

     

      and port number does not match those of S, with which the original

     

      outgoing session was established. Similarly, if B simply starts

     

      sending UDP requests to A's public address, then NAT A will discard

     

      these messages.

     

     

      Suppose A starts sending UDP requests to B's public address, however,

     

      and simultaneously relays a request through server S to B, asking B

     

      to start sending UDP requests to A's public address. A's outgoing

     

      messages directed to B's public address (138.76.29.7:31000) will

     

      cause NAT A to open up a new communication session between A's

     

      private address and B's public address. At the same time, B's

     

      messages to A's public address (155.99.25.11:62000) will cause NAT B

     

      to open up a new communication session between B's private address

     

      and A's public address. Once the new UDP sessions have been opened

     

      up in each direction, client A and B can communicate with each other

     

      directly without further reference to or burden on the "introduction"

     

      server S

     

      大致就如此。以下是上述过程的一些伪代码,不太容易懂,不如看前面的论述。

      下面将讨论,AB同时发包的另外一个办法,以及关于静态端口映射、谁能做"中介"

    是否可以建立TCP通信等细节及其引申。

     

     

    const TCHAR* ServerIP   = _T("61.10.10.10"); //中介服务器,事先配置的

    const UINT   ServerPort = 4500;           //中介服务器UDP端口,事先配置的

     

    TCHAR   PubIP[256]; //对方机器的代理网关的公网IP

    UINT   PubPort;   //对方机器的代理网关的公网映射端口

     

    UINT   ClientPort = 4501;           //ABUDP端口

     

    连接服务器用UDP,下面是客户方(ABSocket)

     

    class CClientSocket : public CSocket

    {

    public:

    virtual void OnReceive( int nErrCode );

    }

     

    void CClientSocket::OnReceive( int nErrCode )

    {

    int     nFlag, *pFlag; //UDP包标志,位于包头4个字节

    TCHAR   PeerIP[128];   //UDP对方的IP

    UINT     PeerPort;     //UDP对方的端口

    char     RecBuf[64];   //假定包尺寸为64

     

    //此处分析出对方IP和端口,即PeerIPPeerPort

    SOCKADDR sa;

    int     SockAddrLen = sizeof(sa);

    GetPeerName( &sa, &SockAddrLen );

    //sa中取出PeerIPPeerPort

     

    Receive( RecBuf, 64 );

    pFlag = (int*)RecBuf;

    nFlag = *pFlag;

    if( lstrcmp( PeerIP, ServerIP ) == 0 ) //如果是服务器返回的信息

    {

    switch( nFlag )

    {

    case 0:   //标识和服务器连接成功,RecBuf是服务器返回的其它在线用户

      //的信息(包括对方的公网IP及端口),这里假定是B的信息

      //RecBuf取出B的代理网关的IP和端口放入PeerIPPeerPort

      lstrcpy( PubIP, PeerIP ); //PeerIP应该是对方的代理网关的公网IP

      PubPort = PeerPort;     //同时PeerPort应该是对方的代理网关的公网映射端口

     

      //接下来,给B的代理公网IP发一个UDP,填充RecBuf,并使标志为0

      SendTo( RecBuf, 64, PubPort, PubIP );

      //马上叫服务器通知B,要BA发一个UDP包,填充RecBuf,并使标志为1

      SendTo( RecBuf, 64, ServerPort, ServerIP );

      break;

    case 1: //标识是来自服务器的通知,叫我(B)发一个UDPA

      //RecBuf里有A的代理公网IP和端口,取出来,放入PeerIPPeerPort

      lstrcpy( PubIP, PeerIP ); //PeerIP应该是对方的代理网关的公网IP

      PubPort = PeerPort;     //同时PeerPort应该是对方的代理网关的公网映射端口

      //A发一个UDP包,填充RecBuf,并使标志为1

      SendTo( RecBuf, 64, PubPort, PubIP );

      break;

      .

      .

      .

    default:

      break;

    }

    }

    else //其它对等客户返回的信息

    {

    switch( nFlag )

    {

    case 0: //标识直接收到A发的UDP

      break;

    case 1: //标识是B发回的UDP包,但,是靠服务器通知B发的

      //至此,可以判断AB互相是否连接成功,就是判断A发给BUDPB收到,而B

      //AUDPA也收到,那么连接就是成功的。否则重复上面的过程。

      break;

      .

      .

      .

    default:

      break;

    }

    }

    }

     

    客户方先连接服务器,服务器收到后,返回所有在线的对等点用户信息

    CClientSocket cs;

    char sBuf[64]; //sBuf的包标识为0,你可以填充其它任何信息,比如用户信息

    cs.Create( ClientPort, SOCK_DGRAM );

    cs.SendTo( sBuf, 64, ServerPort, ServerIP );

     

    class CServerSocket : public CSocket

    {

    public:

    virtual void OnReceive( int nErrCode );

    }

     

    void CServerSocket::OnReceive( int nErrCode )

    {

    int     nFlag, *pFlag; //UDP包标志,位于包头4个字节

    TCHAR   PeerIP[128];   //UDP对方的IP

    UINT     PeerPort;     //UDP对方的端口

    char     RecBuf[64];   //假定包尺寸为64

     

    //此处分析出对方IP和端口,即PeerIPPeerPort

    SOCKADDR sa;

    int     SockAddrLen = sizeof(sa);

    GetPeerName( &sa, &SockAddrLen );

    //sa中取出PeerIPPeerPort

     

    Receive( RecBuf, 64 );

    pFlag = (int*)RecBuf;

    nFlag = *pFlag;

    switch( nFlag )

    {

    case 0: //标识有客户连接

    //给该客户返回所有在线用户

    break;

    case 1: //来自A的通知,要我通知B,叫B发一个UDP包给A的公网IP

    //RecBuf中取出BIP和端口放入PeerIPPeerPort,填充RecBuf,标志为1

    SendTo( RecBuf, 64, PeerPort, PeerIP );

    break;

    }

    }

  • 相关阅读:
    Android SwitchButton(滑动开关)
    创建您自己的Maven模板
    Bag标签成一条线的代码来实现中国字
    rabbitmq的java简单的实现
    【七】注入框架RoboGuice使用:(Your First Custom Binding)
    Sqlmap渗透测试是常用语句
    Android NOtification 使用(震动 闪屏 铃声)
    Android loader 详解
    Android实现获取本机中所有图片
    Android保存图片到系统图库
  • 原文地址:https://www.cnblogs.com/allanyang/p/296728.html
Copyright © 2020-2023  润新知