轉載:file:///C:/Documents%20and%20Settings/Administrator/%E6%A1%8C%E9%9D%A2/%E5%85%B3%E4%BA%8ETCP%E6%89%93%E6%B4%9E%E6%8A%80%E6%9C%AF(P2P)%20-%20SomeTimes%20-%20%E5%8D%9A%E5%AE%A2%E9%A2%91%E9%81%93%20-%20CSDN.NET.htm
建立穿越NAT裝置的p2p的TCP串連只比UDP複雜一點點,TCP協議的"打洞"從協議層來看是與UDP的"打洞"過程非常相似的。儘管如此,基於TCP協議的打洞至今為止還沒有被很好的理解,這也造成了對其提供支援的NAT裝置不是很多。在NAT裝置支援的前提下,基於TCP的"打洞"技術實際上與基於UDP的"打洞"技術一樣快捷、可靠。實際上,只要NAT裝置支援的話,基於TCP的p2p技術的健壯性將比基於UDP的技術的更強一些,因為TCP協議的狀態機器給出了一種標準的方法來精確的擷取某個TCP session的生命期,而UDP協議則無法做到這一點。
一. 通訊端和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參數,該參數用於區分連接埠重用還是地址重用,在這樣的系統裡面,上述所有的參數必須都設定才行。
二. 開啟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通訊過程不再繼續等待是否有新的串連聯入。
與UDP不同的是,使用UDP協議的每個用戶端只需要一個通訊端即可完成與伺服器S通訊,並同時與多個p2p用戶端通訊的任務,而TCP用戶端必須處理多個通訊端綁定到同一個本地TCP連接埠的問題,。
現在來看更加實際的一種情景,A與B分別位於不同的NAT裝置後面,並且假定連接埠號碼是TCP協議的連接埠號碼,而不是UDP的連接埠號碼。用戶端向彼此公網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串連。
三. 從應用程式的角度來看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系統都會按照第二種結果來處理。
下面就是非轉載部分了,我看後的感想:(如沒有特殊聲明,一律是在windows環境下)
這個所謂的"洞"就是SOCKET,通訊端.
連接埠複用就是在一個SOCKET上既可以listen()也可以connect().
有一點需要說明,在我看來所謂的連接埠有兩種形式,一種是主動串連的,一種是被動串連的.也就是說當我去串連某伺服器的某連接埠的時候,我自身也會開啟一個連接埠,這樣才能進行通訊.我們一般知道的都是被動串連的連接埠,而主動串連的連接埠是由系統隨機分配的.不信的話你可以開啟一個網頁,然後開啟CMD視窗輸入"netstat -an",你會發現有一個或者幾個資訊,意思就是本地的XXX連接埠串連到遠端80連接埠(Web服務),我說的也就是這個意思了.但,實際情況卻更複雜,本地開的連接埠在外部的訪問不到的,因為從本地連接埠發送的資料要經過路由,而路由經過分析包後知道是建立串連,所以路由又開了一個連接埠用於接收外部的包,那麼自己的連接埠(內網連接埠)和路由開的連接埠(外網連接埠)會形成一個映射,是這個樣子.
"打洞",就是說:
兩個電腦A和B與伺服器S,A和B都與S建立串連,此時有兩個SOCKET,A和S算一個(叫做S_AS),B和S算一個(叫做S_BS),這兩個都設定了連接埠複用
A和B分別建立新的SOCKET(設定連接埠複用)並調用listen(),就是A建立新的SOCKET綁定到S_AS的連接埠上監聽(內網連接埠),B也一樣
A和B通過伺服器分別拿到對方的外網IP和連接埠號碼(伺服器擷取,主動串連的,路由開的連接埠)
告訴伺服器我們(A和B)都準備好了,這時伺服器發出指令,準備P2P.
此時A和B分別去串連對方,就是,A去串連S_BS的外網連接埠,B去串連S_AS的外網連接埠,至少有一方可以串連到另一方,串連建立.
(監聽自己的內網連接埠,串連對方的外網連接埠)
至於為什麼能串連上,請看上面文章中我標註紅色字型的描述.
做過網路編程的人都應該知道,TCP之所以可靠是因為在建立串連的時候要經過"三向交握"(當然還有其他因素),要事先打好招呼,這三個資料包分別叫做SYN/ACK/RST(說錯了別怪我),由於A和B都是處在內網,彼此相隔兩個路由器(自己這邊一個,對方那邊一個),建立串連的時候自己發送的SYN包能通過自己的路由,卻不能通過對方的路由,所以包被丟棄,而此時對方也發送一個SYN包過來,因為自己已經發送了SYN包,所以自己這邊的路由會認為對方發來的SYN包是回應我的SYN包,所以不會丟棄包,繼續傳遞給自己,而自己也會把對方發來的包當做回應自己的包,所以做處理(當然不是正常的三向交握,SYN和SYN),還會發送ACK給對方,然後對方給我RST包,再然後,串連建立......
想來想去怎麼都是覺得是在忽悠路由器,忽悠忽悠就建立串連了.
最後的結果只有一種:
A串連上了S_BS的連接埠,或者,B串連上了S_AS的連接埠,總之A和B通了.
據資料所知貌似沒有都串連上的情況.
查閱資料就學到這些,也不知道對不對,有空敲代碼試試看~~~