UDP/TCP穿越NAT的P2P通信方法研究(UDP/TCP打洞 Hole Punching

 http://www.cnblogs.com/LeoWong/archive/2009/09/25/1574265.html

 

內容概述:在p2p通信領域中,由NAT(Network Address Translation,網絡地址轉換)引起的問題已經衆所周知了,它會導致在NAT內部的p2p客戶端在無論以何種有效的公網ip都無法訪問的問題。雖 然目前已經發展出多種穿越NAT的技術,但相關的技術文檔卻很少,用來證明這些技術的穩定性和優點的實際數據更少。本文的目的在於描述和分析在實際中運用 得最廣泛、最可靠同時也是最簡單的一種NAT穿越技術,該技術通常被稱爲“打洞”技術。目前,“打洞”技術已經在UDP通信領域中得到了廣泛的理解和應 用,在此,也將討論如何利用它實現可靠的p2p的TCP流通信。在收集了大量的“打洞”技術可以穿越的NAT設備和網絡的數據以後,我們發現82%的已測 NAT設備支持UDP形式的“打洞”穿越,64%的已測NAT設備支持TCP流形式的“打洞”穿越。由於重量級p2p應用程序(如,VOIP、BT、在線 遊戲等)的用戶需求量持續上升,並且該事實也已經引起了NAT設備生產廠商的廣泛關注,因此,我們認爲未來會有越來越多的NAT設備提供對“打洞”穿越技 術的支持。

1、介紹

用戶量高速增長以及大量安全問題的巨大壓力迫使Internet技術不斷向前發展,但是這些新興的技術很大程度地增加了應用程序開發的成本和複雜 性。Internet最初的地址體系是每個節點有一個唯一不變的全局地址,可以通過該地址直接與任何其它的節點進行通信,而現如今,該地址體系已經被新的 實際上廣泛使用的地址體系所替換,新的地址體系是由全局地址域和通過NAT接入全局地址域的大量私有地址域組成。在新的地址體系中(如圖1所示),只有在 “main”全局地址域中的節點可以在網絡中很容易地與任何其它的擁有全局地址的節點通信,因爲該節點擁有全局的、唯一的、可路由的地址。在私有網絡中的 節點可以與在同一個私有網絡中的其它節點進行通信,並且在通常情況下可以向全局地址中的某個“著名”的節點發起TCP連接或發送UDP數據包。NAT設備 在此扮演的角色就是爲從內網向公網發起的連接的節點分配臨時的轉發session,將來自內網的數據包的地址和端口轉換爲公網的地址和端口,將來自公網的 數據包的地址和端口轉換爲內網的端口和地址,同時NAT將屏蔽所有未經授權的來自公網的數據包。

新的Internet地址體系非常適合於“客戶端/服務器”這樣的通信模式,一個典型的C/S通信模式是:客戶端在內網(私有地址域),服務器 在公網(全局地址域),通過NAT將內網和公網連接起來。這種地址體系使得在不同內網(私有地址域)中的兩個節點很難直接通信,而這恰恰是p2p應用 (如,電話會議或在線遊戲)中最基本的要求。很顯然,我們需要一種方法即使在NAT設備存在的前提下,仍然能夠無障礙地實現p2p通信。
在不同內網的兩個節點之間建立p2p連接的最有效的方法就是“打洞”。該技術在基於UDP的應用程序中得到了廣泛的應用,同樣的,該技術也可以 用於基於TCP的應用程序。有趣的是,與“打洞”字面上的意思剛好相反,該技術不會影響到內網的安全。事實上,“打洞”技術使得p2p軟件的絕大部分功能 都在NAT設備默認的安全策略的控制之下,這些都由NAT設備建立的session來管理。本文闡述了適用於UDP和TCP的“打洞”技術,並詳細描述了 重要“打洞”過程中,應用程序和NAT設備之間的行爲。
不幸的是,由於NAT設備的響應和行爲不是標準的,所以沒有任何技術可以穿越現有的所有NAT設備。本文提供了一些在現有NAT設備上進行“打 洞”的實驗結果。我們收集的數據來自於互聯網上使用了“NAT Check”工具並在大量不同生產廠商的NAT設備上進行“打洞”實驗的用戶。由於數據是來自於一個叫做“self-selecting”的用戶社區,或 許不會完全代表在Internet上真正部署和使用的NAT設備,但是結果無論如何還是很令人興奮的。
在做基本的“打洞”操作評估的時候,我們應該指出在現有的NAT設備“打洞”的複雜度上,不同的複雜度會有不同的結果。但目前我們把討論的重點 集中於開發最簡單的,可以應用於任何網絡拓撲結構的、穩定的、有正確NAT響應的NAT設備上的“打洞”技術。我們有意避免使用一些“聰明的小把戲”通過 欺騙某些NAT設備來達到短期內穿越較多的NAT設備,但從長期來看會引起網絡未知錯誤的技術。
儘管引入IPv6會極大地增加互聯網的地址空間,從而減少對於NAT設備的需求量,但短期內IPv6確實增加了對NAT設備的需求量,因爲 NAT設備本身提供了一種方便的方法進行IPv4與IPv6地址域轉換。另外私有網絡上建立匿名和加密訪問節點也有利於組織機構的安全性以及不受外界幹 擾,這些都意味着NAT還將存在相當長的一段時間。同樣,防火牆技術也不會由於有了足夠的ip地址而消失,IPv6的防火牆仍然會默認丟掉所有未經授權的 數據包,仍然可以讓在IPv6環境下工作的應用程序“打洞”。
本文接下來的部分按照如下的方式組織:第二章介紹基本的NAT穿越概念和術語;第三章介紹UDP“打洞”過程;第四章介紹TCP“打洞”過程; 第五章介紹支持“打洞”的NAT設備必須具有那些特性;第六章介紹我們在目前流行的NAT設備上的“打洞”實驗結果;第七章討論相關的網絡問題;第八章全 文總結以及結束語。
2、基本概念本節介紹了本文使用到的基本的NAT術語,着重描述了適用於UDP和TCP兩種協議的通用的NAT穿越技術。
2.1、NAT術語
本文絕大部分術語和分類來自於RFC 2663定義,另外一些來自於較新的RFC 3489中的定義。
理解session是很重要的。一個TCP或UDP的session endpoint是由一個IP地址,端口號組成,每個session是由兩個session endpoint構成。從內網節點的角度來看,一個session由4部分組成分別爲:本地IP,本地端口,遠端IP,遠端端口。session的方向通 常代表了數據包的初始流動的方向;對於TCP來說就是SYN包的流向,對於UDP來說就是第一個用戶數據包的流向。
NAT有很多種,但最普遍的一種類型叫做“傳統”NAT,或者“向外”NAT。他們在內網和公網之間提供了一個“不對稱”橋的映射。“向 外”NAT在默認情況下只允許向外的session穿越NAT:
從外向內的的數據包都會被丟棄掉,除非NAT設備事先已經定義了這些從外向內的數據包是已存在的內網session的一部分。
“外向”NAT會造成p2p協議的混亂,因爲當p2p的雙方決定向在不同NAT後面的對方開始通信的時候,無論哪一方試圖初始化一個 session,另一方的NAT都會拒絕這個請求。NAT穿越的核心思想就是讓p2p的雙方的NAT看上去都是“向外”的NAT。
“向外”NAT有兩種類型:(1)“基礎”NAT,該NAT只轉換IP地址,不轉換端口號。(2)NAPT(Network Address/Port Translation)NAPT轉換整個session endpoints。由於NAPT允許內網的多個節點通過共享的方式使用同一個的公共的IP地址,因此,支持NAPT的NAT設備纔會越來越多。儘管本文 通篇討論的內容都是基於支持NAPT的NAT設備的,但這些規律和技術同樣適用於“基礎”NAT。
2.2 轉發方式
最可靠但同時也是效率最低的p2p穿越NAT進行通信的方法是採用類似C/S方式的轉發。假定兩個節點A和B每個節點都有向外的TCP或UDP 連接,聯入公共的已知服務器S,S的公網IP地址是18.181.0.31,端口號是1234(如圖2所示),每個客戶端位於不同的私有內網中,並且它們 的NAT設備妨礙了客戶端之間直接的p2p連接。做爲對直連方案的替代方案,兩個客戶端可以利用公共的服務器S進行消息的轉發。例如,A爲了將消息送給 B,A只需將消息發給S,然後由S轉發給B,這一過程將使用A與B事先與S建立好的連接。
轉發方式通常只能在雙方客戶端都連接到服務器的時候有效。這種方式的缺點在於,它假定服務器的處理能力和網絡帶寬以及通信延遲都是理想的情況 下,不會受到客戶端個數的影響。但是,由於沒有其它的方法能夠像轉發方式那樣,可以穿越現存的所有NAT設備,因此在構建高可靠性的p2p系統的時候,通 過服務器轉發的方式依舊是一個非常有用的保證系統可靠性的方法。TURN協議定義瞭如何實現安全的轉發方式。
2.3 反向連接方式
一些p2p的應用程序採用了直接但是有所限制的技術來實現NAT穿越,該技術叫做“反向連接”,這是用於當兩個節點聯入服務器S的時候,只有一 個一個節點在NAT設備的後面(如圖3所示)。如果A希望建立與B的連接,那麼A可以直接聯入B,因爲B是在公網中存在的,沒有經過NAT轉換,而且A的 NAT設備也允許A直接由內網發起向外網的連接。如果B希望建立與A的連接,很不幸,A的NAT設備會阻止該操作,此時,B可以藉助於轉發服務器S,向A 發送“反向連接”請求,由A“主動”連接B,從而達到A與B的p2p通信的目的。
儘管該技術的侷限性非常明顯,但是使用已知的服務器做爲中介輔助p2p客戶端雙方進行p2p連接的思想已經成爲了更加通用的“打洞”技術的基本 思想。
3 UDP打洞方式
即使兩個p2p客戶端都位於NAT設備後面,UDP打洞方式也能夠通過已知的服務器實現p2p客戶端直連。該技術在RFC 3027的第5.1節中曾有所提及,在網絡上可以找到對其較模糊的描述,在最近的IP協議實驗中得到應用,在多種在線遊戲協議中得到了應用。
3.1 集中服務器
打洞技術假定客戶端A和B可以與公網內的已知的集中服務器建立UDP連接(可以互發UDP數據包)。當一個客戶端在S上登陸的時候,服務器記錄 下該客戶端的兩個endpoints(IP地址,UDP端口),一個是該客戶端確信自己是通過該ip和端口與服務器S進行通信的,另一個是服務器S記錄下 的由服務器“觀察”到的該客戶端實際與自己通信所使用的ip和端口。我們可以把前一個endpoint看作是客戶端的內網ip和端口,把後一個 endpoint看作是客戶端的內網ip和端口經過NAT轉換後的公網ip和端口。服務器可以從客戶端的登陸消息的消息體中得到該客戶端的內網 endpoint相關信息,可以通過對登陸消息的IP或UDP頭得到該客戶端的公網endpoint。如果該客戶端不是位於NAT設備後面,那麼採用上述 方法得到的兩個endpoint的值應該完全相同。
也有一些“弱智”的NAT設備會掃描UDP數據包的包體,尋找4字節的位域,看上去很像IP地址的位域,並且把它們改爲與IP頭一樣的地址。爲 了避免這種行爲的NAT設備對UDP數據包包體的修改,應用程序可以採用直接對IP地址的值進行加密的方式騙過NAT設備的檢查。
3.2 建立p2p的session
假定A要發起對B的直接連接,“打洞”過程如下所示:(endpoint指ip地址和端口的配對)(1)A最初不知道如何向B發起連接,於是A 向服務器S發送消息,請求S幫助建立與B的UDP連接。
(2)S將含有B的公網和內網的endpoint發給A,同時,S將含有A的公網和內網的endpoint的用於請求連接的消息也發給B。一旦 這些消息順利到達,A與B就都知道了對方的公網和內網的endpoint。
(3)當A收到由S發來的包含B的公網和內網endpoint的消息,A開始向這些B的endpoint發送UDP數據包,並且A會自動鎖定第 一個給出響應的B的endpoint。同理,當B收到由S發來的A的公網和內網endpoint以後,也會開始向A的公網和內網的endpoint發送 UDP數據包,並且自動鎖定第一個得到A的迴應的endpoint。由於A與B的互相向對方發送UDP數據包的操作是異步的,所以A和B發送數據包的時間 先後並沒有嚴格的時序要求。
下面我們就來看一下這三個角色之間是如何進行UDP“打洞”的。在這裏我們分爲三種具體情景來討論:第一種也是最“簡單”的一種情景,兩個客戶 端都位於同一個NAT設備後面,位於同一個內網中;第二種也是最普遍的一種情景,兩個客戶端分別位於不同的NAT設備後面,分屬不同的內網;第三種是客戶 端位於兩層NAT設備之後,通常最上層的NAT是由ISP網絡提供商,第二層的NAT是家用的NAT路由器之類的設備。
通常情況下由應用程序自身確定的網絡物理層連接方式是很困難的,有時甚至是不可能的,即使是上述的若干種情景下可以穿越NAT,也只是代表在一 定時期內有效,而不是永久有效的。諸如STUN之類的網絡協議或許可以提供必要的NAT信息,但在遇到多層NAT設備的時候,通常這些信息也不是完全完整 和有效的。儘管如此,只要NAT設備的響應是“合理”的,在通常情況下“打洞”技術還是能夠在應用程序對網絡狀況一無所知的前提下自動適用於多數場 合。(“合理”的NAT響應將在第五章中詳細討論)
3.3 p2p客戶端位於同一個NAT設備後面
首先假設兩個客戶端位於同一個NAT設備後面,並且位於相同的內網(相同的私有IP地址域)如圖4所示。A與S建立了UDP連接,經過NAT轉 換後,A的公網端口被映射爲62000。B同樣與S建立了UDP連接,公網端口映射爲62005。
(圖4)
假設A想通過服務器S做爲介紹人,發起對B的連接。A向S發出消息請求與B進行連接。S將B的公網endpoint(即公網ip和port)以 及內網endpoint(即內網ip和port)發給A,同時把A的公網、內網的endpoints發給B。由A和B發往對方公網endpoint的 UDP數據包能否被對方收到,這取決於當前的NAT是否支持“髮夾”轉換(hairpin轉換,也就是同一臺設備,不同端口之間的UDP數據包能否到達, 詳見3.5節)。但是A與B往對方內網endpoint發送的UDP數據包是一定可以到達的,無論如何,內網數據包不需要路由,並且速度更快。A與B有很 大的可能性採用內網的endpoint進行常規的p2p通信。
假定NAT設備支持“髮夾”轉換,應用程序也忽略由內網endpoint的連接,那麼A、B會採用公網endpoint做爲p2p通信的連接, 這勢必會造成數據包無謂地經過NAT設備,這是一種對資源的浪費。我們會在第六節討論這種情況,畢竟支持“髮夾”轉換的NAT設備還遠沒有對“打洞”技術 支持的NAT設備多。就目前的網絡情況而言,應用程序在“打洞”的時候,最好還是把公網endpoint和內網endpoint都實驗一下。
3.4 p2p客戶端位於不同的NAT設備後面
假定A與B在不同的NAT設備後面,分屬不同的內網,如圖5所示。A與B都經由各自的NAT設備與服務器S建立了UDP連接,A與B的本地端口 號均爲4321,服務器S的公網端口號爲1234。在“向外”session中,A的公網IP被映射爲155.99.25.11,公網端口爲62000, B的公網IP被映射爲138.76.29.7,公網端口爲31000。
如下所示:客戶端A-->本地IP:10.0.0.1,本地端口:4321,公網IP:155.99.25.11,公網端口:62000 客戶端B-->本地IP:10.1.1.3,本地端口:4321,公網IP:138.76.29.7,公網端口:31000
(圖 5)
在A向服務器S發送的登陸消息體中,會包含A的內網endpoint信息,即10.0.0.1:4321;服務器S會記錄下A的內網 endpoint,同時會把自己觀察到的A的公網endpoint記錄下來,即155.99.25.11:62000。同理,服務器S會記錄下B的內網 endpoint,10.1.1.3:4321和由S觀察到的B的公網endpoint,138.76.29.7:31000。無論A與B二者任何一方向 S發送p2p連接請求,服務器都會將其記錄下來的上述的公網、內網endpoint發送給A、B。
由於A、B分屬不同的內網,它們彼此的內網endpoint無法在公網中路由,所以發往各自內網endpoint的UDP數據包會發送到錯誤的 主機或者根本不存在的主機。因此應用程序對於收到的消息必須經過授權和過濾,只有通過授權的的消息才能是從對方的endpoint發出來的,例如,可以在 消息中加入對方的程序名稱、加密算法,或者至少是一個雙方都從服務器S上的預先得到的隨機數字。
現在假定A的第一個消息將發往B的公網endpoint,如圖5所示。該消息途經A的NAT設備,並在該設備上生成了一個“向外”的 session。新的session源endpoint是10.0.0.1:4321該endpoint和A與服務器S的建立連接的時候NAT生成的源 endpoint一樣,但它的目的endpoint不同。如果A的NAT設備給出的響應是“友好”的,那麼A的NAT設備將保留A的內網 endpoint,並且所有來自A的源endpoint(10.0.0.1:4321)的數據包都沿用A與S事先建立起來的session,公網 endpoint均爲(155.99.25.11:62000)。A向B的公網endpoint發送消息的過程就是“打洞”的過程,從A的內網的角度來看 應爲從(10.0.0.1:4321)發往(138.76.29.7:31000),從A的在其NAT設備上建立的session來看,是從 (155.99.25.11:62000)發到(138.76.29.7:31000)。
如果A發給B的公網endpoint的消息包在B向A發送消息包之前到達B的NAT設備,B的NAT會認爲A發過來的消息是未經授權的公網消 息,會丟棄掉該數據包。B發往A的消息包根上述的過程一樣,會在B的NAT上建立一個(10.1.1.3:4321,155.99.25.11: 62000)的session(通常也會沿用B與S連接時建立的session,只是該session現在不光可以接受由S發給B的消息,還可以接受從A 的NAT設備-155.99.25.11:6200發來的消息)
一旦A與B都向對方的NAT在公網上的endpoint發送了數據包,就打開了A與B之間的“洞”,A與B向對方的公網endpoint發送數 據,等效爲向對方的客戶端直接發送UDP數據包了。一旦應用程序確認已經可以通過往對方的公網endpoint發送數據包的方式讓數據包到達NAT後面的 目的應用程序,程序會自動停止繼續發送用於“打洞”的數據包,轉而開始真正的p2p數據傳輸。
3.5 p2p客戶端位於多層NAT設備後面
有的網絡拓撲結構包含了多個NAT設備,如果沒有掌握該拓撲結構的詳細信息,兩個客戶端之間是無法建立“最優化”的p2p路由的。現在我們來討 論最後一種情況,如圖6所示。假定NAT C是由ISP(Internet Service Provider)提供的工業級的NAT設備,NAT C提供將多個下屬的用戶NAT或用戶節點映射到有限的幾個公網IP的服務,NAT A和NAT B做爲NAT C的內網節點將把用戶的家庭網絡或內部網絡接入NAT C的內網,然後用戶的內部網絡就可以經由NAT C訪問公網了。從這種拓撲結構上來看,只有服務器S與NAT C是真正擁有公網可路由IP地址的設備,而NAT A和NAT B所使用的“公網”IP地址,實際上是由ISP服務提供商設定的(相對於NAT C而言)內網地址(本位的後續部分我把這個由ISP提供的內網地址相對於NAT A和NAT B稱之爲“僞”公網地址),同理隸屬於NAT A與NAT B的客戶端,相對與NAT A,NAT B而言,它們處於NAT A,NAT B的內網,以此類推,客戶端可以放到到多層NAT設備後面。客戶端A和客戶端B發起對服務器S的連接的時候,就會依次在NAT A和NAT B上建立向外的session,而NAT A、NAT B要聯入公網的時候,會在NAT C上再建立向外的session。
(圖 6)
現在假定客戶端A和B希望通過UDP“打洞”完成兩個客戶端的p2p直連。最優化的路由策略是客戶端A向客戶端B的“僞公網”IP上發送數據 包,即ISP服務提供商指定的內網IP,NAT B的“僞”公網endpoint,10.0.1.2:55000。由於從服務器S的角度只能觀察到真正的公網地址,也就是NAT A,NAT B在NAT C建立的session的真正的公網地址155.99.25.11:62000以及155.99.25.11:62005,所以非常不幸,客戶端A與客戶 端B是無法通過服務器S知道這些“僞”公網的地址的。而且即使客戶端A和B通過某種手段可以得到NAT A和NAT B的“僞”公網地址,我們仍然不建議採用上述的“最優化”的打洞方式,這是因爲這些地址是由ISP服務提供商提供的或許會存在與客戶端本身所在的內網地址 重複的可能性。(例如:NAT A的內網的IP地址域恰好與NAT A在NAT C的“僞”公網IP地址域重複,這樣就會導致打洞數據包無法發出的問題)
因此客戶端別無選擇,只能使用由公網服務器S觀察到的A,B的公網endpoint進行“打洞”操作,用於“打洞”的數據包將由NAT C進行轉發,這裏NAT C是否支持“髮夾”轉換或“環路”轉換非常重要,否則數據包將無法由NAT C轉發給NAT A和NAT B,進而無法到達客戶端A和B。當客戶端A向客戶端B的公網endpoint(155.99.25.11:62005)發送UDP數據包的時候,NAT A首先把數據包的源endpoint由A的內網endpoint(10.0.0.1:4321)轉換爲“僞”公網endpoint(10.0.1.1: 45000),現在數據包到了NAT C,NAT C應該可以識別出來該數據包是要發往自身轉換過的公網endpoint,如果NAT C可以給出“合理”響應的話,NAT C將把該數據包的源endpoint改爲155.99.25.11:62000,目的endpoint改爲10.0.1.2:55000,即NAT B的“僞”公網endpoint,NAT B最後會將收到的數據包發往客戶端B。同樣,由B發往A的數據包也會經過類似的過程。也有很多NAT設備不支持類似這樣的“髮夾”轉換,但是已經有越來越 多的NAT設備生產廠商開始加入對該轉換的支持。
3.6 UDP在空閒狀態下的超時問題
由於UDP轉換協議提供的“洞”不是絕對可靠的,多數NAT設備內部都有一個UDP轉換的空閒狀態計時器,如果在一段時間內沒有UDP數據通 信,NAT設備會關掉由“打洞”操作打出來的“洞”,做爲應用程序來講如果想要做到與設備無關,就最好在穿越NAT的以後設定一個穿越的有效期。很遺憾目 前沒有標準有效期,這個有效期與NAT設備內部的配置有關,最短的只有20秒左右。在這個有效期內,即使沒有p2p數據包需要傳輸,應用程序爲了維持該 “洞”可以正常工作,也必須向對方發送“打洞”維持包。這個維持包是需要雙方應用都發送的,只有一方發送不會維持另一方的session正常工作。除了頻 繁發送“打洞”維持包以外,還有一個方法就是在當前的“洞”有效期過期之前,p2p客戶端雙方重新“打洞”,丟棄原有的“洞”,這也不失爲一個有效的方 法。

4 關於TCP打洞技術建立穿越NAT設備的p2p的TCP連接只比UDP複雜一點點,TCP協議的“打洞”從協議層來看是與UDP的“打洞”過程非常相似 的。儘管如此,基於TCP協議的打洞至今爲止還沒有被很好的理解,這也造成了對其提供支持的NAT設備不是很多。在NAT設備支持的前提下,基於TCP的 “打洞”技術實際上與基於UDP的“打洞”技術一樣快捷、可靠。實際上,只要NAT設備支持的話,基於TCP的p2p技術的健壯性將比基於UDP的技術的 更強一些,因爲TCP協議的狀態機給出了一種標準的方法來精確的獲取某個TCP session的生命期,而UDP協議則無法做到這一點。4.1 套接字和TCP端口的重用實現基於TCP協議的p2p“打洞”過程中,最主要的問題不是來自於TCP協議,而是來自於來自於應用程序的API接口。這是由 於標準的伯克利(Berkeley)套接字的API是圍繞着構建客戶端/服務器程序而設計的,API允許TCP流套接字通過調用connect()函數來 建立向外的連接,或者通過listen()和accept函數接受來自外部的連接,但是,API不提供類似UDP那樣的,同一個端口既可以向外連接,又能 夠接受來自外部的連接。而且更糟的是,TCP的套接字通常僅允許建立1對1的響應,即應用程序在將一個套接字綁定到本地的一個端口以後,任何試圖將第二個 套接字綁定到該端口的操作都會失敗。爲了讓TCP“打洞”能夠順利工作,我們需要使用一個本地的TCP端口來監聽來自外部的TCP連接,同時建立多個向外 的TCP連接。幸運的是,所有的主流操作系統都能夠支持特殊的TCP套接字參數,通常叫做“SO_REUSEADDR”,該參數允許應用程序將多個套接字 綁定到本地的一個endpoint(只要所有要綁定的套接字都設置了SO_REUSEADDR參數即可)。BSD系統引入了SO_REUSEPORT參 數,該參數用於區分端口重用還是地址重用,在這樣的系統裏面,上述所有的參數必須都設置纔行。4.2 打開p2p的TCP流假定客戶端A希望建立與B的TCP連接。我們像通常一樣假定A和B已經與公網上的已知服務器S建立了TCP連接。服務器記錄下來每個 聯入的客戶端的公網和內網的endpoints,如同爲UDP服務的時候一樣。從協議層來看,TCP“打洞”與UDP“打洞”是幾乎完全相同的過程。1、 客戶端A使用其與服務器S的連接向服務器發送請求,要求服務器S協助其連接客戶端B。2、S將B的公網和內網的TCP endpoint返回給A,同時,S將A的公網和內網的endpoint發送給B。3、客戶端A和B使用連接S的端口異步地發起向對方的公網、內網 endpoint的TCP連接,同時監聽各自的本地TCP端口是否有外部的連接聯入。4、A和B開始等待向外的連接是否成功,檢查是否有新連接聯入。如果 向外的連接由於某種網絡錯誤而失敗,如:“連接被重置”或者“節點無法訪問”,客戶端只需要延遲一小段時間(例如延遲一秒鐘),然後重新發起連接即可,延 遲的時間和重複連接的次數可以由應用程序編寫者來確定。5、TCP連接建立起來以後,客戶端之間應該開始鑑權操作,確保目前聯入的連接就是所希望的連接。 如果鑑權失敗,客戶端將關閉連接,並且繼續等待新的連接聯入。客戶端通常採用“先入爲主”的策略,只接受第一個通過鑑權操作的客戶端,然後將進入p2p通 信過程不再繼續等待是否有新的連接聯入。

(圖 7)

與UDP 不同的是,使用UDP協議的每個客戶端只需要一個套接字即可完成與服務器S通信,並同時與多個p2p客戶端通信的任務,而TCP客戶端必須處理多個套接字 綁定到同一個本地TCP端口的問題,如圖7所示。現在來看更加實際的一種情景,A與B分別位於不同的NAT設備後面,如圖5所示,並且假定圖中的端口號是 TCP協議的端口號,而不是UDP的端口號。圖中向外的連接代表A和B向對方的內網endpoint發起的連接,這些連接或許會失敗或者無法連接到對方。 如同使用UDP協議進行“打洞”操作遇到的問題一樣,TCP的“打洞”操作也會遇到內網的IP與“僞”公網IP重複造成連接失敗或者錯誤連接之類的問題。 客戶端向彼此公網endpoint發起連接的操作,會使得各自的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連接。4.3 從應用程序的角度來看TCP“打洞”從應用程序的角度來看,在進行TCP“打洞”的時候都發生了什麼呢?假定A首先向B發出SYN包,該包發往B的公網 endpoint,並且被B的NAT設備丟棄,但是B發往A的公網endpoint的SYN包則通過A的NAT到達了A,然後,會發生以下的兩種結果中的 一種,具體是哪一種取決於操作系統對TCP協議的實現:(1)A的TCP實現會發現收到的SYN包就是其發起連接並希望聯入的B的SYN包,通俗一點來說 就是“說曹操,曹操到”的意思,本來A要去找B,結果B自己找上門來了。A的TCP協議棧因此會把B做爲A向B發起連接connect的一部分,並認爲連 接已經成功。程序A調用的異步connect()函數將成功返回,A的listen()等待從外部聯入的函數將沒有任何反映。此時,B聯入A的操作在A程 序的內部被理解爲A聯入B連接成功,並且A開始使用這個連接與B開始p2p通信。由於收到的SYN包中不包含A需要的ACK數據,因此,A的TCP將用 SYN-ACK包迴應B的公網endpoint,並且將使用先前A發向B的SYN包一樣的序列號。一旦B的TCP收到由A發來的SYN-ACK包,則把自 己的ACK包發給A,然後兩端建立起TCP連接。簡單的說,第一種,就是即使A發往B的SYN包被B的NAT丟棄了,但是由於B發往A的包到達了A。結果 是,A認爲自己連接成功了,B也認爲自己連接成功了,不管是誰成功了,總之連接是已經建立起來了。(2)另外一種結果是,A的TCP實現沒有像(1)中所 講的那麼“智能”,它沒有發現現在聯入的B就是自己希望聯入的。就好比在機場接人,明明遇到了自己想要接的人卻不認識,誤認爲是其它的人,安排別人給接走 了,後來才知道是自己錯過了機會,但是無論如何,人已經接到了任務已經完成了。然後,A通過常規的listen()函數和accept()函數得到與B的 連接,而由A發起的向B的公網endpoint的連接會以失敗告終。儘管A向B的連接失敗,A仍然得到了B發起的向A的連接,等效於A與B之間已經聯通, 不管中間過程如何,A與B已經連接起來了,結果是A和B的基於TCP協議的p2p連接已經建立起來了。第一種結果適用於基於BSD的操作系統對於TCP的 實現,而第二種結果更加普遍一些,多數linux和windows系統都會按照第二種結果來處理。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章