TCP打洞技術


建立穿越NAT設備的p2p的TCP連接只比UDP復雜一點點,TCP協議的“打洞”從協議層來看是與UDP 的“打洞”過程非常相似的。盡管如此,基於TCP協議的打洞至今為止還沒有被很好的理解,這也 造成了對其提供支持的NAT設備不是很多。在NAT設備支持的前提下,基於TCP的“打洞”技術實際上 與基於UDP的“打洞”技術一樣快捷、可靠。實際上,只要NAT設備支持的話,基於TCP的p2p技術 的健壯性將比基於UDP的技術的更強一些,因為TCP協議的狀態機給出了一種標准的方法來精確的 獲取某個TCP session的生命期,而UDP協議則無法做到這一點。
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參數,該參數用於區分 端口重用還是地址重用,在這樣的系統里面,上述所有的參數必須都設置才行。
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連接。
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系統都會按照第二種結果來處理。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM