P2P可以是一種通信模式、一種邏輯網絡模型、一種技術、甚至一種理念。在P2P網絡中,所有通信節點的地位都是對等的,每個節點都扮演着客戶機和服務器雙重角色,節點之間通過直接通信實現文件信息、處理器運算能力、存儲空間等資源的共享。P2P網絡具有分散性、可擴展性、健壯性等特點,這使得P2P技術在信息共享、即時通訊、協同工作、分布式計算、網絡存儲等領域都有廣闊的應用。
由於主機可能位於防火牆或NAT之后,在進行P2P通信之前,我們需要進行檢測以確認它們之間能否進行P2P通信以及如何通信。這種技術通常稱為NAT穿透(NAT Traversal)。最常見的NAT穿透是基於UDP的技術。
1、應用層網關技術
NAT帶來的問題之一就是限制了使用高層協議的兩端P2P通信,因為NAT不允許外部的Peer節點主動連接或發送數據包給NAT后面的主機。ALG是解決NAT對應用層協議無感知的一個最常用方法。
ALG技術是利用NAT本身的支持來進行NAT的穿越,這個方案有很大限制,主要的原因是ALG都是為特定協議的特定規范版本而開發的,然而不管是協議本身,還是協議的數量都在變化,這就使得ALG適應性不強
2、中間件技術
這是一種通過開發通用方法解決NAT穿越問題的努力。與前者不同之處是,AGL技術中NAT網關是這一解決方案的唯一參與者,而中間件技術中客戶端會參與網關公網映射信息的維護。UPnP就是這樣一種方法,UPnP中文全稱為通用即插即用,是一個通用的網絡終端與網關的通信協議,具備信息發布和管理控制的能力。
3、打洞技術
Hole Punching技術是工作在運輸層的技術,可以屏蔽上層應用層的差異,並且不需要NAT網關特定的支持,因此其通用性比較強,應用性也比較廣。
3.1、反向鏈接技術
適用場景:通信雙方只有一方位於NAT設備之后
客戶端A位於NAT之后,NAT設備重新分配了TCP端口62000,由於B擁有外網地址,A可以直接通過TCP鏈接到B。但B想要與A通信,就需要通過服務器給A轉發一個連接請求,反過來請求A連接到B(即反向鏈接
),A在收到從服務器轉發過來的請求以后,會主動向B發起一個連接請求,這樣在NAT設備上就會建立起關於這個連接的相關表項,使AB之間能夠正常通信,建立TCP連接。

3.2、基於UDP協議的P2P打洞技術
3.2.1、原理
UDP打洞技術是通過中間服務器的協助在各自的NAT網關上建立相關的表項,使P2P連接的雙方發送的報文能夠直接穿透對方的NAT網關,從而實現P2P客戶端互連。如果兩台位於NAT設備后面的P2P客戶端希望在自己的NAT網關上打個洞,那么他們需要一個協助者——集中服務器,並且還需要一種用於打洞的Session建立機制。
集中服務器本質上是一台被設置在公網上的服務器,建立P2P的雙方都可以直接訪問到這台服務器。 位於NAT網關后面的客戶端A和B都可以與一台已知的集中服務器建立連接,並通過這台集中服務器了解對方的信息並中轉各自的信息。 同時集中服務器的另一個重要作用在於判斷某個客戶端是否在NAT網關之后,集中服務器可以從客戶端的登陸消息中得到該客戶端的內網相關信息,還可以通過登陸消息的IP頭和UDP頭得到該客戶端的外網相關信息。如果該客戶端不是位於NAT設備后面,那么采用上述方法得到的兩對地址二元組信息是完全相同的。
P2P的Session建立的原理就是通過集中服務器將包含各自的內外網二元組發送給對方,這樣彼此都能知道對方內外網的地址了。然后A開始向B的內外網地址發送UDP數據包,並且A會自動鎖定第一個給出相應的B的地址的二元組。
3.2.2、場景一:兩客戶端位於同一NAT設備后(相同內網)
當A向集中服務器發出消息請求與B進行連接,集中服務器將B的外網地址二元組以及內網地址二元組發給A,同時把A的外網以及內網的地址二元組信息發給B。A和B發往對方公網地址二元組信息的UDP數據包不一定會被對方收到,這取決於當前的NAT設備是否支持不同端口之間的UDP數據包能否到達(即Hairpin轉換特性),無論如何A與B發往對方內網的地址二元組信息的UDP數據包是一定可以到達的,內網數據包不需要路由,且速度更快。A與B推薦采用內網的地址二元組信息進行常規的P2P通信。
就目前的網絡情況而言,應用程序在“打洞”的時候,最好還是把外網和內網的地址二元組都嘗試一下。如果都能成功,優先以內網地址進行連接。
Hairpin技術需要NAT網關支持,它能夠讓兩台位於同一台NAT網關后面的主機,通過對方的公網地址和端口相互訪問。目前有很多NAT設備不支持該技術,這種情況下,NAT網關在一些特定場合下將會阻斷P2P穿越NAT的行為,打洞的嘗試是無法成功的。
3.2.3、場景二:兩客戶端位於不同NAT設備后面(不同內網)
在客戶端向服務器發送的登陸消息中,包含有客戶端的內網地址二元組信息;服務器會記錄下客戶端的內網地址二元組信息,同時會把自己觀察到的客戶端的外網地址二元組信息記錄下來。
無論A、B二者中任何一方向服務器發送P2P連接請求,服務器都會將起記錄下來內外網地址發送給A和B。
A拿到B的外網地址后,開始發送消息,這個過程就是“打洞”。如果A發給B的外網地址二元組的消息包在B向A發送消息包之前到達B的NAT設備,B的NAT設備會認為A發過來的消息是未經授權的外網消息,並丟棄該數據包,B發往A的消息包也會在B的NAT設備上建立一個【10.10.1.3:4321 155.99.25.11:62000】會話。
一旦A與B都向對方的NAT設備在外網上的地址二元組發送了數據包,就打開了A與B之間的“洞”,A與B向對方的外網地址發送數據,等效為向對方的客戶端直接發送UDP數據包了。一旦應用程序確認已經可以通過往對方的外網地址發送數據包的方式讓數據包到達NAT后面的目的應用程序,程序會自動停止繼續發送用於“打洞”的數據包,轉而開始真正的P2P數據傳輸。
3.2.4、場景三:兩客戶端位於兩層(或多層)NAT設備之后(不同內網)
此種情景最典型的部署情況就像這樣:最上層的NAT設備通常是由網絡提供商(ISP)提供,下層NAT設備是家用路由器。
從這種拓撲結構上來看,只有服務器與NAT C是真正擁有公網可路由IP地址的設備,而NAT A和NAT B所使用的公網IP地址,實際上是由ISP服務提供商設定的(相對於NAT C而言)內網地址(我們將這種由ISP提供的內網地址稱之為“偽”公網地址)。
最優化的路由策略當然是通過“偽公網”IP通信,但不幸的是A、B是無法通過服務器知道這些“偽公網”地址,即使知道,也不建議,因為這些地址是由ISP提供的,存在重復的可能性(例如:NAT A的內網的IP地址域恰好與NAT A在NAT C的“偽”公網IP地址域重復,這樣就會導致打洞數據包無法發出的問題)。
因此客戶端只能使用公網服務器觀察到的A、B的公網地址進行“打洞”操作,用於“打洞”的數據包將由NAT C進行轉發。
3.3、基於TCP協議的P2P打洞技術
這種技術從協議層面和UDP類似,只要NAT設備支持的話,基於TCP的P2P技術的健壯性將比基於UDP技術的更強一些,因為TCP協議的狀態機給出了一種標准的方法來精確的獲取某個TCP session的生命期,而UDP協議則無法做到這一點。
3.3.1、套接字和TCP端口重用
API不提供類似UDP那樣的,同一個端口既可以向外連接,又能夠接受來自外部的連接。
TCP的套接字通常僅允許建立1對1的響應,即應用程序在將一個套接字綁定到本地的一個端口以后,任何試圖將第二個套接字綁定到該端口的操作都會失敗。
為了能夠打洞順利,需要使用一個TCP端口監聽來自外部分TCP連接,同時建立多個向外的TCP連接。幸運的是,所有主流操作系統支持特殊的TCP套接字參數SO_REUSEADDR
。
3.3.2、打開P2P的TCP流
假定客戶端A希望建立與B的TCP連接。我們像通常一樣假定A和B已經與公網上的已知服務器建立了TCP連接。服務器記錄下來每個接入的客戶端的公網和內網的地址二元組,如同為UDP服務的時候一樣。
從協議層面來看,兩者過程幾乎完全相同。

與UDP不同的是,因為使用UDP協議的每個客戶端只需要一個套接字即可完成與服務器的通信,而TCP客戶端必須處理多個套接字綁定到同一個本地TCP端口的問題。
客戶端向彼此公網地址二元組發起連接的操作,會使得各自的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.3.3、從應用程序角度來看TCP打洞
假定A首先向B發出SYN包,該包發往B的公網地址二元組,並且被B的NAT設備丟棄,但是B發往A的公網地址二元組的SYN包則通過A的NAT到達了A,然后,會發生以下的兩種結果中的一種,具體是哪一種取決於操作系統對TCP協議的實現:
- A會認為自己連接成功了,本來A要找B,結果B自己送上門了,此時connect()會成功返回,A的listen()等待從外部聯入的函數將沒有任何反應。此時,B聯入A的操作在A程序的內部被理解為A聯入B連接成功,A的TCP將用SYN-ACK包回應B,B也認為自己連接成功了,連接建立起來。
- A沒有前面那么聰明,沒有發現聯入的B就是自己希望的。A通過常規的listen()函數和accept()函數得到與B的連接,而由A發起的向B的公網地址二元組連接將以失敗告終。
第一種結果適用於基於BSD的操作系統對於TCP的實現,而第二種結果更加普遍一些,多數Linux和Windows系統都會按照第二種結果來處理。
參考鏈接:
Peer-to-Peer Communication Across Network Address Translators