不同區域網路內經Internet的P2P通訊技術總結

來源:互聯網
上載者:User

以下將要用到一個叫做NAT的重要名詞,先做點解釋。
NAT是Net Address Translation(網路位址轉譯)的簡稱,就是說,區域網路通常靠一個具有公網IP的代理閘道伺服器連到Internet共用上網。區域網路內的機器並不具備公網IP地址,它只有內網地址,假設它要和Internet上的HTTP伺服器通訊,代理網關便會建立一個連接埠來和這個網內機器關聯,並通過這個連接埠來和HTTP伺服器交換資料。最終,網內機器->代理網關->HTTP伺服器,在一個會話期間,各自的連接埠保持了映射關係,特別是代理網關和網內機器的連接埠映射,使得代理網關不會把接收到的資料向網內轉寄時,發錯了機器。
區域網路內的機器在網關處,就是靠NAT來映射連接埠並實現Internet串連,因此,NAT也直接被稱為“連接埠映射”。連接埠映射之後,在一個會話期間保持,對於TCP串連是直到串連斷開才銷毀,而對於UDP,卻存在一個不定的生存期,例如2秒。

如果兩台機器A和B,分別處於兩個區域網路內,它們要通過Internet通訊,這就是P2P(點到點)串連通訊。
目前的Internet使用IPv4協議,採用32位IP地址,主要被用來進行C/S形式的通訊,需要共用的資源集中放於Internet伺服器上。IPv4對於P2P分布式資源共用的支援,極不友好。首先,32位IP地址已經不敷使用,公網IP地址日趨緊張,只能使用區域網路共用公網IP的方式,區域網路正是為了臨時應對IP耗盡而出現的,長遠的解決辦法是研究IPv6。其次,分別處於兩個區域網路內的機器要通訊,由於對方沒有公網IP,直接呼叫對方是不可能的,必須藉助第三方“中介”(機器或者軟體)間接地連通,解決辦法下列幾種:
第一:實現區域網路內的資料連結層協議,就是寫一個類似於TCP/IP的協議,由它來代替Windows系統裡的TCP/IP協議,由它直接基於網卡硬體擷取資料。這是十分複雜的。
第二:用Internet上的公網伺服器中轉資料,但對於大資料量的中轉,顯然受到伺服器和網路的負載極限的限制。
第三:依靠Internet上的公網伺服器做“媒人”,將這兩台分別處於不同區域網路的機器相互介紹給對方,在它們建立串連之後,伺服器即脫離關係。這種方式下,伺服器把A的NAT連接埠映射關係告訴B,又把B的NAT連接埠映射關係告訴A,這樣AB相互知道對方的連接埠映射關係之後,就能建立串連。因為A和B各自的連接埠映射關係是靠各自的代理網關動態建立的,動態建立的映射連接埠不得不告知對方。
第四:上面的第三種辦法,也可以採用靜態連接埠映射方式,這樣就不需要中繼伺服器對A和B做介紹。在各方的代理網關上,可以在代理工具裡將某個連接埠(如1350)和區域網路內的某台機器(如內網IP為200.200.200.100,連接埠1360)做好靜態映射,這樣,代理網關會自動地將出入於1350連接埠的資料發往200.200.200.100的1360連接埠。當然,通訊之前,必須對對方的連接埠映射關係做配置。有多少台網內機器要通訊,就得映射多少個不同的連接埠,同時在另一個區域網路內的機器就要做多少個配置。在區域網路內搭建HTTP、FTP等伺服器就是通過靜態映射連接埠來實現的,這個連接埠一般不是HTTP、FTP的預設80和23,所以對這類網站的訪問往往會在URL裡加上連接埠號碼。
由此可見,上述前兩種辦法在簡單應用中是不可取的,只有後兩種可行。它們又各有缺點,第三種動態映射連接埠,需要增加中間伺服器,第四種靜態映射連接埠,在需要通訊的各方機器很多的情況下,做手工連接埠映射和配置都是很繁瑣的,並且一方添加一台機器,就需要在其餘對方增加配置。
採用動態和靜態相結合的辦法是可以推想的,然而其可行性還必須經過測試。可以這樣設計,為了讓所有通訊機器彼此知曉並定位。我們可以在區域網路裡,只對一台機器在代理網關處做靜態連接埠映射,本區域網路內的機器都向它登記。而兩個區域網路各自只做一項對對方的映射配置。兩個區域網路之間,沒有靜態映射連接埠的機器要通訊,就靠有映射的機器來擔當“介紹”。
就區域網路和NAT的問題實際上還很多,比如各自的區域網路的結構不同,區域網路裡可能又有子區域網路,區域網路可能是NAT代理結構,但也可能是HTTP代理,Sock4、Sock5代理等結構,NAT又分嚴格的和非嚴格NAT,嚴格NAT限制很多,更不便於P2P。不過,軟體不能實現的地方,可以考慮改變硬體結構,例如將嚴格NAT變為非嚴格NAT。如果硬體改變不得,那麼Internet整體上就有10%的系統不能實現P2P,除非等到正處於研發的IPv6協議出來。
P2P要解決的唯一技術難題是如何發現、定位和定址對方,就是如何穿透NAT、HTTP、Sock等代理和如何穿透防火牆找到對方並建立起通訊的問題。由於絕大多數區域網路是NAT代理結構,所以前面對NAT論述比較詳細,也是網上討論最多的話題,相比之下,穿透Http、Sock代理就簡單一些。此外,穿透NAT發現對等點的辦法還有一些,例如多播,但由於現有Internet對多播並不友好,同時多播是無串連和不可靠的,其實現有難度。
許多軟體都是按照上述一些技術實現了P2P通訊,著名的有MSN、QQ和BitTorrent下載軟體等。

實際上,圍繞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()得到A的IP及連接埠,但得到的IP和連接埠應該是A的代理網關的公網PublicIP及其映射連接埠NatPort,該映射連接埠就是A的代理網關為A的本次UDP通訊臨時分配的Nat連接埠。可以斷言,得到的連接埠一定不是A的內網IP和內網UDP連接埠。
  伺服器然後將A的公網IP、映射連接埠、使用者資訊等儲存到(記憶體列表或者資料庫),這樣標誌著A已經上線。伺服器馬上將其它線上的使用者資訊發回給A,包括其它使用者的代理網關的公網IP及Nat連接埠。A同樣將線上使用者的這些資訊儲存並顯示為列表,期待A使用者做出選擇。
  對於B,同樣有上述的上線過程。
  當A使用者做出選擇,要和線上的B使用者通訊時,A首先發UDP包給B的公網IP及Nat連接埠,並立即發一個UDP包給伺服器,讓伺服器去通知B,叫B給A也發一個UDP包。
  換句話說,1、A發包給PublicB, 2、A發包給Server,3、Server發包給PublicB,4、B發包給PublicA。
  上面的敘述用到了"Public"字樣,它代表代理網關的公網IP及其映射連接埠。
  
  由於A和B各自的網關都儲存了各自的連接埠映射關係,發到網關的資料,網關會按照這個映射關係轉寄給A和B。
  當A和B都分別收到對方發來的UDP包以後,串連宣告成功,伺服器即可以脫離,AB即可以用UDP通訊。

  何以如此麻煩?

  A在發UDP包給Server上線時,A的網關(A.Gate)就分配一個Nat連接埠(A.NatPort)給A,用於A和Server間的本次UDP會話,但A的網關明確標記,這個Nat連接埠,僅能用於A和Server之間的UDP通訊,不能挪著它用。並且,這個臨時分配的連接埠,只能保持一個很短的時效,也許是一兩秒吧。這個時間內,如果A與Server沒有任何通訊,那麼這個映射連接埠就宣告無效。下次,A和Server又要通訊時,A的網關又會重新分配一個新的連接埠。這段表明三點:
  1、A與Server的通訊,需要A網關分配Nat連接埠來中轉。
  2、Nat連接埠只能用於A和Server間的通訊。
  3、Nat連接埠存在生存期,長時間A和Server無通訊,該連接埠即宣告無效。
  就是這些麻煩,使得我們的串連過程必須繞很多彎。
  A和B的通訊,就是藉助事先AB分別與Server串連時,在各自的網關處建立的連接埠映射來通訊。為避免上面的2、3點麻煩,A和B在初次串連時,必須幾乎同時向對方發包。
  如果A、B不同時發包給對方,它們各自的網關就會慮掉對方的包,因為該包不是Server發來的包,叫做不請自來的包。
  並且,即便AB各自的網關不慮掉非Server發來的包,它們各自的Nat連接埠也有一個時效。那麼A與Server,B與Server就不得不發心跳包,以維持各自的映射連接埠,保證其不失效。
  上面的過程中,如果A和B建立串連失敗,可以迴圈這個過程,直到一個有限的次數之後,仍不能串連則宣告失敗。本文一部分的原文如下:

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

   大致就如此。以下是上述過程的一些虛擬碼,不太容易懂,不如看前面的論述。
   下面將討論,A和B同時發包的另外一個辦法,以及關於靜態連接埠映射、誰能做"中介"、
是否可以建立TCP通訊等細節及其引申。

const TCHAR*  ServerIP   = _T("61.10.10.10");  //中繼伺服器,事先配置的
const UINT    ServerPort = 4500;               //中繼伺服器UDP連接埠,事先配置的

TCHAR   PubIP[256]; //對方機器的代理網關的公網IP
UINT    PubPort;    //對方機器的代理網關的公網映射連接埠

UINT    ClientPort = 4501;               //A和B的UDP連接埠

串連伺服器用UDP,下面是客戶方(AB的Socket)

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和連接埠,即PeerIP和PeerPort
 SOCKADDR  sa;
 int       SockAddrLen = sizeof(sa);
 GetPeerName( &sa, &SockAddrLen );
 //從sa中取出PeerIP和PeerPort

 Receive( RecBuf, 64 );
 pFlag = (int*)RecBuf;
 nFlag = *pFlag;
 if( lstrcmp( PeerIP, ServerIP ) == 0 )  //如果是伺服器返回的資訊
 {
  switch( nFlag )
  {
  case  0:   //標識和伺服器串連成功,RecBuf是伺服器返回的其它線上使用者
   //的資訊(包括對方的公網IP及連接埠),這裡假定是B的資訊
   //從RecBuf取出B的代理網關的IP和連接埠放入PeerIP和PeerPort
   lstrcpy( PubIP, PeerIP ); //PeerIP應該是對方的代理網關的公網IP
   PubPort = PeerPort;       //同時PeerPort應該是對方的代理網關的公網映射連接埠

   //接下來,給B的代理公網IP發一個UDP包,填充RecBuf,並使標誌為0
   SendTo( RecBuf, 64, PubPort, PubIP );
   //馬上叫伺服器通知B,要B給A發一個UDP包,填充RecBuf,並使標誌為1
   SendTo( RecBuf, 64, ServerPort, ServerIP );
   break;
  case  1: //標識是來自伺服器的通知,叫我(B)發一個UDP給A
   //RecBuf裡有A的代理公網IP和連接埠,取出來,放入PeerIP和PeerPort
   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發給B的UDP包B收到,而B發
   //給A的UDP包A也收到,那麼串連就是成功的。否則重複上面的過程。
   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和連接埠,即PeerIP和PeerPort
 SOCKADDR  sa;
 int       SockAddrLen = sizeof(sa);
 GetPeerName( &sa, &SockAddrLen );
 //從sa中取出PeerIP和PeerPort

 Receive( RecBuf, 64 );
 pFlag = (int*)RecBuf;
 nFlag = *pFlag;
 switch( nFlag )
 {
 case  0:  //標識有客戶串連
  //給該客戶返回所有線上使用者
  break;
 case  1:  //來自A的通知,要我通知B,叫B發一個UDP包給A的公網IP
  //從RecBuf中取出B的IP和連接埠放入PeerIP和PeerPort,填充RecBuf,標誌為1
  SendTo( RecBuf, 64, PeerPort, PeerIP );
  break;
 }
}

相關文章

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.