關(guān)于采用UDP協(xié)議進(jìn)行打洞以進(jìn)行P2P會話的原理,我本來想寫一篇文章作說明,但是現(xiàn)在已經(jīng)有一篇文章把原理性的東西解釋清楚了,我在這里不再作這部分的重復(fù),可以參見這里: P2P 之 UDP穿透NAT的原理與實(shí)現(xiàn)(附源代碼)--http://www./Class/hack/0512182034513804825.htm 下面解釋一下上面的文章中沒有提及或者說我覺得比較欠缺的地方. 私有地址/端口和公有地址/端口:我們知道,現(xiàn)在大部分網(wǎng)絡(luò)采用的都是NAPT(Network Address/Port Translator)了,這個東東的作用是一個對外的對話在經(jīng)過NAT之后IP地址和端口號都會被改寫,在這里把一次會話中客戶自己認(rèn)為在使用的IP地址和端口號成為私有地址/端口,而把經(jīng)過NAPT之后被改寫的IP地址和端口號稱為公有地址/端口.或者可以這么理解,私有地址/端口是你家里人對你的昵稱而公有地址/端口則是你真正對外公開的名字.如何獲得用戶的私用地址/端口號,這個很簡單了,而要得到公有地址/端口號就要在連接上另一臺機(jī)器之后由那臺機(jī)器看到的IP地址和端口號來表示. 如果明白了上面的東西,下面進(jìn)入我們的代碼,在這里解釋一下關(guān)鍵部分的實(shí)現(xiàn): 客戶端首先得到自己的私有地址/終端,然后向server端發(fā)送登陸請求,server端在得到這個請求之后就可以知道這個client端的公有地址/終端,server會為每一個登陸的client保存它們的私有地址/端口和公有地址/端口. OK,下面開始關(guān)鍵的打洞流程.假設(shè)client A要向client B對話,但是A不知道B的地址,即使知道根據(jù)NAT的原理這個對話在第一次會被拒絕,因?yàn)閏lient B的NAT認(rèn)為這是一個從沒有過的外部發(fā)來的請求.這個時候,A如果發(fā)現(xiàn)自己沒有保存B的地址,或者說發(fā)送給B的會話請求失敗了,它會要求server端讓B向A打一個洞,這個B->A的會話意義在于它使NAT B認(rèn)為A的地址/端口是可以通過的地址/端口,這樣A再向B發(fā)送對話的時候就不會再被NAT B拒絕了.打一個比方來說明打洞的過程,A想來B家做客,但是遭到了B的管家NAT B的拒絕,理由是:我從來沒有聽我家B提過你的名字,這時A找到了A,B都認(rèn)識的朋友server,要求server給B報(bào)一個信,讓B去跟管家說A是我的朋友,于是,B跟管家NAT B說,A是我認(rèn)識的朋友,這樣A的訪問請求就不會再被管家NAT B所拒絕了.簡而言之,UDP打洞就是一個通過server保存下來的地址使得彼此之間能夠直接通信的過程,server只管幫助建立連接,在建立間接之后就不再介入了. 好了,原理性的東西解釋到這里,附件中有一個完整的P2P演示程序,命令行模式下,包括server端和client端,在運(yùn)行的時候首先啟動server端,然后打開幾個client端分別登陸,之后彼此之間就可以相互通信了.程序在本機(jī)上測試通過,也就是測試的環(huán)境server和client都是一臺機(jī)器,還沒有在不同的機(jī)器上測試過,不知道會不會有問題:) 下載地址: http://www./Files/converse/P2PDemo.rar 參考資料: 1)P2P 之 UDP穿透NAT的原理與實(shí)現(xiàn)(附源代碼)-http://www./Class/hack/0512182034513804825.htm 2)王艷平< 建立穿越NAT設(shè)備的p2p的TCP連接只比UDP復(fù)雜一點(diǎn)點(diǎn),TCP協(xié)議的'打洞'從協(xié)議層來看是與UDP的'打洞'過程非常相似的,。盡管如此,,基于TCP協(xié)議的打洞至今為止還沒有被很好的理解,這也造成了對其提供支持的NAT設(shè)備不是很多,。在NAT設(shè)備支持的前提下,,基于TCP的'打洞'技術(shù)實(shí)際上與基于UDP的'打洞'技術(shù)一樣快捷、可靠,。實(shí)際上,,只要NAT設(shè)備支持的話,,基于TCP的p2p技術(shù)的健壯性將比基于UDP的技術(shù)的更強(qiáng)一些,,因?yàn)門CP協(xié)議的狀態(tài)機(jī)給出了一種標(biāo)準(zhǔn)的方法來精確的獲取某個TCP session的生命期,,而UDP協(xié)議則無法做到這一點(diǎn),。 一. 套接字和TCP端口的重用 實(shí)現(xiàn)基于TCP協(xié)議的p2p'打洞'過程中,,最主要的問題不是來自于TCP協(xié)議,而是來自于來自于應(yīng)用程序的API接口。這是由于標(biāo)準(zhǔn)的伯克利(Berkeley)套接字的API是圍繞著構(gòu)建客戶端/服務(wù)器程序而設(shè)計(jì)的,,API允許TCP流套接字通過調(diào)用connect()函數(shù)來建立向外的連接,或者通過listen()和accept函數(shù)接受來自外部的連接,,但是,,API不提供類似UDP那樣的,同一個端口既可以向外連接,,又能夠接受來自外部的連接,。而且更糟的是,TCP的套接字通常僅允許建立1對1的響應(yīng),即應(yīng)用程序在將一個套接字綁定到本地的一個端口以后,,任何試圖將第二個套接字綁定到該端口的操作都會失敗,。 為了讓TCP'打洞'能夠順利工作,,我們需要使用一個本地的TCP端口來監(jiān)聽來自外部的TCP連接,,同時建立多個向外的TCP連接,。幸運(yùn)的是,,所有的主流操作系統(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,。 與UDP不同的是,,使用UDP協(xié)議的每個客戶端只需要一個套接字即可完成與服務(wù)器S通信,,并同時與多個p2p客戶端通信的任務(wù),,而TCP客戶端必須處理多個套接字綁定到同一個本地TCP端口的問題,,如圖所示,。 現(xiàn)在來看更加實(shí)際的一種情景,,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è)備,到達(dá)A,,從而建立起A與B的p2p連接。 三. 從應(yīng)用程序的角度來看TCP'打洞' 從應(yīng)用程序的角度來看,,在進(jìn)行TCP'打洞'的時候都發(fā)生了什么呢,?假定A首先向B發(fā)出SYN包,該包發(fā)往B的公網(wǎng)endpoint,,并且被B的NAT設(shè)備丟棄,,但是B發(fā)往A的公網(wǎng)endpoint的SYN包則通過A的NAT到達(dá)了A,,然后,,會發(fā)生以下的兩種結(jié)果中的一種,具體是哪一種取決于操作系統(tǒng)對TCP協(xié)議的實(shí)現(xiàn): (1)A的TCP事先會發(fā)現(xiàn)收到的SYN包就是其發(fā)起連接并希望聯(lián)入的B的SYN包,,通俗一點(diǎn)來說就是'說曹操,曹操到'的意思,,本來A要去找B,,結(jié)果B自己找上門來了。A的TCP協(xié)議棧因此會把B做為A向B發(fā)起連接connect的一部分,,并認(rèn)為連接已經(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的包到達(dá)了A,。結(jié)果是,A認(rèn)為自己連接成功了,,B也認(rèn)為自己連接成功了,,不管是誰成功了,,總之連接是已經(jīng)建立起來了,。 (2)另外一種結(jié)果是,,A的TCP實(shí)現(xiàn)沒有像(1)中所講的那么'智能',,它沒有發(fā)現(xiàn)現(xiàn)在聯(lián)入的B就是自己希望聯(lián)入的。就好比在機(jī)場接人,,明明遇到了自己想要接的人卻不認(rèn)識,誤認(rèn)為是其它的人,,安排別人給接走了,后來才知道是自己錯過了機(jī)會,,但是無論如何,,人已經(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的實(shí)現(xiàn),而第二種結(jié)果更加普遍一些,,多數(shù)linux和windows系統(tǒng)都會按照第二種結(jié)果來處理,。 下面就是非轉(zhuǎn)載部分了,我看后的感想:(如沒有特殊聲明,一律是在windows環(huán)境下) 這個所謂的'洞'就是SOCKET,套接字. 端口復(fù)用就是在一個SOCKET上既可以listen()也可以connect(). 有一點(diǎn)需要說明,在我看來所謂的端口有兩種形式,一種是主動連接的,一種是被動連接的.也就是說當(dāng)我去連接某服務(wù)器的某端口的時候,我自身也會開啟一個端口,這樣才能進(jìn)行通訊.我們一般知道的都是被動連接的端口,而主動連接的端口是由系統(tǒng)隨機(jī)分配的.不信的話你可以打開一個網(wǎng)頁,然后開啟CMD窗口輸入'netstat -an',你會發(fā)現(xiàn)有一個或者幾個信息,意思就是本地的XXX端口連接到遠(yuǎn)程的80端口(Web服務(wù)),我說的也就是這個意思了.但,實(shí)際情況卻更復(fù)雜,本地開的端口在外部的訪問不到的,因?yàn)閺谋镜囟丝诎l(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)都準(zhǔn)備好了,這時服務(wù)器發(fā)出指令,準(zhǔn)備P2P. 此時A和B分別去連接對方,就是,A去連接S_BS的外網(wǎng)端口,B去連接S_AS的外網(wǎng)端口,至少有一方可以連接到另一方,連接建立. (監(jiān)聽自己的內(nèi)網(wǎng)端口,連接對方的外網(wǎng)端口) 至于為什么能連接上,請看上面文章中我標(biāo)注紅色字體的描述. 做過網(wǎng)絡(luò)編程的人都應(yīng)該知道,TCP之所以可靠是因?yàn)樵诮⑦B接的時候要經(jīng)過'三次握手'(當(dāng)然還有其他因素),要事先打好招呼,這三個數(shù)據(jù)包分別叫做SYN/ACK/RST(說錯了別怪我),由于A和B都是處在內(nèi)網(wǎng),彼此相隔兩個路由器(自己這邊一個,對方那邊一個),建立連接的時候自己發(fā)送的SYN包能通過自己的路由,卻不能通過對方的路由,所以包被丟棄,而此時對方也發(fā)送一個SYN包過來,因?yàn)樽约阂呀?jīng)發(fā)送了SYN包,所以自己這邊的路由會認(rèn)為對方發(fā)來的SYN包是回應(yīng)我的SYN包,所以不會丟棄包,繼續(xù)傳遞給自己,而自己也會把對方發(fā)來的包當(dāng)做回應(yīng)自己的包,所以做處理(當(dāng)然不是正常的三次握手,SYN和SYN),還會發(fā)送ACK給對方,然后對方給我RST包,再然后,連接建立...... 想來想去怎么都是覺得是在忽悠路由器,忽悠忽悠就建立連接了. 最后的結(jié)果只有一種: A連接上了S_BS的端口,或者,B連接上了S_AS的端口,總之A和B通了. 據(jù)資料所知貌似沒有都連接上的情況. 查閱資料就學(xué)到這些,也不知道對不對,有空敲代碼試試看~~~ |
|