2)王艷平
建立穿越NAT設(shè)備的p2p的TCP連接只比UDP復(fù)雜一點點,TCP協(xié)議的'打洞'從協(xié)議層來看是與UDP的'打洞'過程非常相似的。盡管如此,基于TCP協(xié)議的打洞至今為止還沒有被很好的理解,這也造成了對其提供支持的NAT設(shè)備不是很多。在NAT設(shè)備支持的前提下,基于TCP的'打洞'技術(shù)實際上與基于UDP的'打洞'技術(shù)一樣快捷、可靠。實際上,只要NAT設(shè)備支持的話,基于TCP的p2p技術(shù)的健壯性將比基于UDP的技術(shù)的更強一些,因為TCP協(xié)議的狀態(tài)機給出了一種標準的方法來精確的獲取某個TCP session的生命期,而UDP協(xié)議則無法做到這一點。
一. 套接字和TCP端口的重用
實現(xiàn)基于TCP協(xié)議的p2p'打洞'過程中,最主要的問題不是來自于TCP協(xié)議,而是來自于來自于應(yīng)用程序的API接口。這是由于標準的伯克利(Berkeley)套接字的API是圍繞著構(gòu)建客戶端/服務(wù)器程序而設(shè)計的,API允許TCP流套接字通過調(diào)用connect()函數(shù)來建立向外的連接,或者通過listen()和accept函數(shù)接受來自外部的連接,但是,API不提供類似UDP那樣的,同一個端口既可以向外連接,又能夠接受來自外部的連接。而且更糟的是,TCP的套接字通常僅允許建立1對1的響應(yīng),即應(yīng)用程序在將一個套接字綁定到本地的一個端口以后,任何試圖將第二個套接字綁定到該端口的操作都會失敗。
為了讓TCP'打洞'能夠順利工作,我們需要使用一個本地的TCP端口來監(jiān)聽來自外部的TCP連接,同時建立多個向外的TCP連接。幸運的是,所有的主流操作系統(tǒng)都能夠支持特殊的TCP套接字參數(shù),通常叫做'SO_REUSEADDR',該參數(shù)允許應(yīng)用程序?qū)⒍鄠€套接字綁定到本地的一個endpoint(只要所有要綁定的套接字都設(shè)置了SO_REUSEADDR參數(shù)即可)。BSD系統(tǒng)引入了SO_REUSEPORT參數(shù),該參數(shù)用于區(qū)分端口重用還是地址重用,在這樣的系統(tǒng)里面,上述所有的參數(shù)必須都設(shè)置才行。
二. 打開p2p的TCP流
假定客戶端A希望建立與B的TCP連接。我們像通常一樣假定A和B已經(jīng)與公網(wǎng)上的已知服務(wù)器S建立了TCP連接。服務(wù)器記錄下來每個聯(lián)入的客戶端的公網(wǎng)和內(nèi)網(wǎng)的endpoints,如同為UDP服務(wù)的時候一樣。從協(xié)議層來看,TCP'打洞'與UDP'打洞'是幾乎完全相同的過程。
1、客戶端A使用其與服務(wù)器S的連接向服務(wù)器發(fā)送請求,要求服務(wù)器S協(xié)助其連接客戶端B。
2、S將B的公網(wǎng)和內(nèi)網(wǎng)的TCP endpoint返回給A,同時,S將A的公網(wǎng)和內(nèi)網(wǎng)的endpoint發(fā)送給B。
3、客戶端A和B使用連接S的端口異步地發(fā)起向?qū)Ψ降墓W(wǎng)、內(nèi)網(wǎng)endpoint的TCP連接,同時監(jiān)聽各自的本地TCP端口是否有外部的連接聯(lián)入。
4、A和B開始等待向外的連接是否成功,檢查是否有新連接聯(lián)入。如果向外的連接由于某種網(wǎng)絡(luò)錯誤而失敗,如:'連接被重置'或者'節(jié)點無法訪問',客戶端只需要延遲一小段時間(例如延遲一秒鐘),然后重新發(fā)起連接即可,延遲的時間和重復(fù)連接的次數(shù)可以由應(yīng)用程序編寫者來確定。
5、TCP連接建立起來以后,客戶端之間應(yīng)該開始鑒權(quán)操作,確保目前聯(lián)入的連接就是所希望的連接。如果鑒權(quán)失敗,客戶端將關(guān)閉連接,并且繼續(xù)等待新的連接聯(lián)入??蛻舳送ǔ2捎?先入為主'的策略,只接受第一個通過鑒權(quán)操作的客戶端,然后將進入p2p通信過程不再繼續(xù)等待是否有新的連接聯(lián)入。
與UDP不同的是,使用UDP協(xié)議的每個客戶端只需要一個套接字即可完成與服務(wù)器S通信,并同時與多個p2p客戶端通信的任務(wù),而TCP客戶端必須處理多個套接字綁定到同一個本地TCP端口的問題,如圖所示。
現(xiàn)在來看更加實際的一種情景,A與B分別位于不同的NAT設(shè)備后面,并且假定端口號是TCP協(xié)議的端口號,而不是UDP的端口號。客戶端向彼此公網(wǎng)endpoint發(fā)起連接的操作,會使得各自的NAT設(shè)備打開新的'洞'允許A與B的TCP數(shù)據(jù)通過。如果NAT設(shè)備支持TCP'打洞'操作的話,一個在客戶端之間的基于TCP協(xié)議的流通道就會自動建立起來。如果A向B發(fā)送的第一個SYN包發(fā)到了B的NAT設(shè)備,而B在此前沒有向A發(fā)送SYN包,B的NAT設(shè)備會丟棄這個包,這會引起A的'連接失敗'或'無法連接'問題。而此時,由于A已經(jīng)向B發(fā)送過SYN包,B發(fā)往A的SYN包將被看作是由A發(fā)往B的包的回應(yīng)的一部分,所以B發(fā)往A的SYN包會順利地通過A的NAT設(shè)備,到達A,從而建立起A與B的p2p連接。
三. 從應(yīng)用程序的角度來看TCP'打洞'
從應(yīng)用程序的角度來看,在進行TCP'打洞'的時候都發(fā)生了什么呢?假定A首先向B發(fā)出SYN包,該包發(fā)往B的公網(wǎng)endpoint,并且被B的NAT設(shè)備丟棄,但是B發(fā)往A的公網(wǎng)endpoint的SYN包則通過A的NAT到達了A,然后,會發(fā)生以下的兩種結(jié)果中的一種,具體是哪一種取決于操作系統(tǒng)對TCP協(xié)議的實現(xiàn):
(1)A的TCP事先會發(fā)現(xiàn)收到的SYN包就是其發(fā)起連接并希望聯(lián)入的B的SYN包,通俗一點來說就是'說曹操,曹操到'的意思,本來A要去找B,結(jié)果B自己找上門來了。A的TCP協(xié)議棧因此會把B做為A向B發(fā)起連接connect的一部分,并認為連接已經(jīng)成功。程序A調(diào)用的異步connect()函數(shù)將成功返回,A的listen()等待從外部聯(lián)入的函數(shù)將沒有任何反映。此時,B聯(lián)入A的操作在A程序的內(nèi)部被理解為A聯(lián)入B連接成功,并且A開始使用這個連接與B開始p2p通信。
由于收到的SYN包中不包含A需要的ACK數(shù)據(jù),因此,A的TCP將用SYN-ACK包回應(yīng)B的公網(wǎng)endpoint,并且將使用先前A發(fā)向B的SYN包一樣的序列號。一旦B的TCP收到由A發(fā)來的SYN-ACK包,則把自己的ACK包發(fā)給A,然后兩端建立起TCP連接。簡單的說,第一種,就是即使A發(fā)往B的SYN包被B的NAT丟棄了,但是由于B發(fā)往A的包到達了A。結(jié)果是,A認為自己連接成功了,B也認為自己連接成功了,不管是誰成功了,總之連接是已經(jīng)建立起來了。
(2)另外一種結(jié)果是,A的TCP實現(xiàn)沒有像(1)中所講的那么'智能',它沒有發(fā)現(xiàn)現(xiàn)在聯(lián)入的B就是自己希望聯(lián)入的。就好比在機場接人,明明遇到了自己想要接的人卻不認識,誤認為是其它的人,安排別人給接走了,后來才知道是自己錯過了機會,但是無論如何,人已經(jīng)接到了任務(wù)已經(jīng)完成了。然后,A通過常規(guī)的listen()函數(shù)和accept()函數(shù)得到與B的連接,而由A發(fā)起的向B的公網(wǎng)endpoint的連接會以失敗告終。盡管A向B的連接失敗,A仍然得到了B發(fā)起的向A的連接,等效于A與B之間已經(jīng)聯(lián)通,不管中間過程如何,A與B已經(jīng)連接起來了,結(jié)果是A和B的基于TCP協(xié)議的p2p連接已經(jīng)建立起來了。
第一種結(jié)果適用于基于BSD的操作系統(tǒng)對于TCP的實現(xiàn),而第二種結(jié)果更加普遍一些,多數(shù)linux和windows系統(tǒng)都會按照第二種結(jié)果來處理。
下面就是非轉(zhuǎn)載部分了,我看后的感想:(如沒有特殊聲明,一律是在windows環(huán)境下)
這個所謂的'洞'就是SOCKET,套接字.
端口復(fù)用就是在一個SOCKET上既可以listen()也可以connect().
有一點需要說明,在我看來所謂的端口有兩種形式,一種是主動連接的,一種是被動連接的.也就是說當我去連接某服務(wù)器的某端口的時候,我自身也會開啟一個端口,這樣才能進行通訊.我們一般知道的都是被動連接的端口,而主動連接的端口是由系統(tǒng)隨機分配的.不信的話你可以打開一個網(wǎng)頁,然后開啟CMD窗口輸入'netstat -an',你會發(fā)現(xiàn)有一個或者幾個信息,意思就是本地的XXX端口連接到遠程的80端口(Web服務(wù)),我說的也就是這個意思了.但,實際情況卻更復(fù)雜,本地開的端口在外部的訪問不到的,因為從本地端口發(fā)送的數(shù)據(jù)要經(jīng)過路由,而路由經(jīng)過分析包后知道是建立連接,所以路由又開了一個端口用于接收外部的包,那么自己的端口(內(nèi)網(wǎng)端口)和路由開的端口(外網(wǎng)端口)會形成一個映射,是這個樣子.
'打洞',就是說:
兩個電腦A和B與服務(wù)器S,A和B都與S建立連接,此時有兩個SOCKET,A和S算一個(叫做S_AS),B和S算一個(叫做S_BS),這兩個都設(shè)置了端口復(fù)用
A和B分別創(chuàng)建新的SOCKET(設(shè)置端口復(fù)用)并調(diào)用listen(),就是A創(chuàng)建新的SOCKET綁定到S_AS的端口上監(jiān)聽(內(nèi)網(wǎng)端口),B也一樣
A和B通過服務(wù)器分別拿到對方的外網(wǎng)IP和端口號(服務(wù)器獲取,主動連接的,路由開的端口)
告訴服務(wù)器我們(A和B)都準備好了,這時服務(wù)器發(fā)出指令,準備P2P.
此時A和B分別去連接對方,就是,A去連接S_BS的外網(wǎng)端口,B去連接S_AS的外網(wǎng)端口,至少有一方可以連接到另一方,連接建立.
(監(jiān)聽自己的內(nèi)網(wǎng)端口,連接對方的外網(wǎng)端口)
至于為什么能連接上,請看上面文章中我標注紅色字體的描述.
做過網(wǎng)絡(luò)編程的人都應(yīng)該知道,TCP之所以可靠是因為在建立連接的時候要經(jīng)過'三次握手'(當然還有其他因素),要事先打好招呼,這三個數(shù)據(jù)包分別叫做SYN/ACK/RST(說錯了別怪我),由于A和B都是處在內(nèi)網(wǎng),彼此相隔兩個路由器(自己這邊一個,對方那邊一個),建立連接的時候自己發(fā)送的SYN包能通過自己的路由,卻不能通過對方的路由,所以包被丟棄,而此時對方也發(fā)送一個SYN包過來,因為自己已經(jīng)發(fā)送了SYN包,所以自己這邊的路由會認為對方發(fā)來的SYN包是回應(yīng)我的SYN包,所以不會丟棄包,繼續(xù)傳遞給自己,而自己也會把對方發(fā)來的包當做回應(yīng)自己的包,所以做處理(當然不是正常的三次握手,SYN和SYN),還會發(fā)送ACK給對方,然后對方給我RST包,再然后,連接建立......
想來想去怎么都是覺得是在忽悠路由器,忽悠忽悠就建立連接了.
最后的結(jié)果只有一種:
A連接上了S_BS的端口,或者,B連接上了S_AS的端口,總之A和B通了.
據(jù)資料所知貌似沒有都連接上的情況.
查閱資料就學到這些,也不知道對不對,有空敲代碼試試看~~~