《通信網(wǎng)絡(luò)程序設(shè)計(jì)》課件第11章_第1頁(yè)
《通信網(wǎng)絡(luò)程序設(shè)計(jì)》課件第11章_第2頁(yè)
《通信網(wǎng)絡(luò)程序設(shè)計(jì)》課件第11章_第3頁(yè)
《通信網(wǎng)絡(luò)程序設(shè)計(jì)》課件第11章_第4頁(yè)
《通信網(wǎng)絡(luò)程序設(shè)計(jì)》課件第11章_第5頁(yè)
已閱讀5頁(yè),還剩106頁(yè)未讀 繼續(xù)免費(fèi)閱讀

下載本文檔

版權(quán)說(shuō)明:本文檔由用戶提供并上傳,收益歸屬內(nèi)容提供方,若內(nèi)容存在侵權(quán),請(qǐng)進(jìn)行舉報(bào)或認(rèn)領(lǐng)

文檔簡(jiǎn)介

第11章P2P技術(shù)11.1P2P技術(shù)概述11.2NAT穿越11.3P2P編程小結(jié)

由于傳統(tǒng)的C/S模式對(duì)服務(wù)器過(guò)于依賴,導(dǎo)致負(fù)載分布不平衡,以及性價(jià)比和擴(kuò)展性都存在一定的問(wèn)題,因此目前一種新的通信模式非常盛行,這就是P2P(Peer-to-Peer)技術(shù)。P2P技術(shù)通過(guò)客戶端進(jìn)行互聯(lián)的方法,將通信任務(wù)的壓力分散開(kāi)來(lái),可以大大優(yōu)化文件下載、流媒體、即時(shí)通信、語(yǔ)音通信等多種網(wǎng)絡(luò)應(yīng)用,是網(wǎng)絡(luò)通信程序方面的程序員必須掌握的一項(xiàng)關(guān)鍵技術(shù)。

本章首先介紹P2P的概念、分類和原理,然后講解了將P2P技術(shù)運(yùn)用于當(dāng)前網(wǎng)絡(luò)需解決的NAT的穿越問(wèn)題,重點(diǎn)介紹了兩種NAT穿越技術(shù)的打洞方法,繼而進(jìn)行了基于TCP打洞的P2P編程實(shí)例。掌握本章學(xué)習(xí)內(nèi)容需緊密結(jié)合對(duì)TCP/IP協(xié)議的IP層工作機(jī)理的深入理解。

P2P正式步入發(fā)展在20世紀(jì)90年代末期,始于美國(guó)波士頓大學(xué)一年級(jí)新生肖恩·范寧編寫(xiě)的Napster音樂(lè)共享程序,現(xiàn)已徹底統(tǒng)治了當(dāng)今的互聯(lián)網(wǎng)。據(jù)統(tǒng)計(jì),互聯(lián)網(wǎng)中大約50%~90%的總流量都來(lái)自于P2P,具有非同一般的意義。11.1P2P技術(shù)概述11.1.1概念

P2P是英文“對(duì)等”的簡(jiǎn)稱,又稱為“點(diǎn)對(duì)點(diǎn)”?!皩?duì)等”技術(shù)是一種網(wǎng)絡(luò)新技術(shù),依賴于網(wǎng)絡(luò)中參與者的計(jì)算能力和帶寬,而不是把依賴都聚集在較少的幾臺(tái)服務(wù)器上。簡(jiǎn)單地說(shuō),P2P直接將人們聯(lián)系起來(lái),讓人們通過(guò)互聯(lián)網(wǎng)直接交互,這使得網(wǎng)絡(luò)上用戶的溝通變得更容易,共享和交互也變得更直接,真正地消除了網(wǎng)絡(luò)連接的中間環(huán)節(jié)。也就是說(shuō),P2P就是人們可以直接連接到其他用戶的計(jì)算機(jī),并交換文件,而不是像過(guò)去那樣連接到服務(wù)器再瀏覽與下載(如圖11-1所示)。同時(shí),P2P也改變了互聯(lián)網(wǎng)現(xiàn)在以P2P工作組大網(wǎng)站為中心的狀態(tài),重返“非中心化”狀態(tài),并把權(quán)力交還給了用戶。

P2P不是一種新的協(xié)議,而是利用現(xiàn)有的網(wǎng)絡(luò)協(xié)議實(shí)現(xiàn)網(wǎng)絡(luò)數(shù)據(jù)或資源信息共享的技術(shù),它使用的可能是TCP、UDP或其他協(xié)議。P2P技術(shù)與C/S模式相比具有許多優(yōu)點(diǎn)。首先,P2P能夠解決C/S模式中存在的服務(wù)器與客戶機(jī)計(jì)算任務(wù)分配不均的問(wèn)題,使負(fù)載均衡,降低服務(wù)器的壓力。其次,P2P具有高性價(jià)比,能夠利用閑置的計(jì)算能力或存儲(chǔ)空間,達(dá)到高性能計(jì)算和海量存儲(chǔ)的目的。另外,P2P可擴(kuò)展性良好,由于不存在服務(wù)器端瓶頸,其擴(kuò)展性大大加強(qiáng)。同時(shí),也應(yīng)當(dāng)注意P2P帶來(lái)了版權(quán)與安全性等問(wèn)題。圖11-1C/S模式與P2P模式結(jié)構(gòu)比較目前,P2P有許多耳熟能詳?shù)闹麘?yīng)用,如文件下載的BitTorrent、迅雷、電驢;流媒體播放器PPLive;即時(shí)通信軟件ICQ、MSN;語(yǔ)音網(wǎng)絡(luò)通信軟件Skype等。11.1.2原理

P2P的設(shè)計(jì)模式總體可以分為兩類,即單純型P2P和混合型P2P架構(gòu),其原理稍有不同。

單純型P2P架構(gòu)沒(méi)有中央服務(wù)器,各個(gè)節(jié)點(diǎn)之間直接交互信息,如圖11-1(b)所示,其優(yōu)點(diǎn)是使用方便,任何一個(gè)安裝了P2P應(yīng)用軟件的計(jì)算機(jī),都可以與其他安裝這個(gè)軟件的計(jì)算機(jī)進(jìn)行P2P通信。它的缺點(diǎn)是沒(méi)有中央服務(wù)器參與協(xié)調(diào),使用范圍比較有限。

混合型P2P架構(gòu)將P2P和C/S模式相結(jié)合。這種結(jié)構(gòu)在單純型的基礎(chǔ)上引入中央服務(wù)器,這里的中央服務(wù)器不同于C/S模式中的服務(wù)器,僅起到促成各節(jié)點(diǎn)協(xié)調(diào)和擴(kuò)展的功能。工作時(shí)安裝了P2P軟件的各個(gè)計(jì)算機(jī)首先登錄中央服務(wù)器連接,告知服務(wù)器自己監(jiān)聽(tīng)的IP地址和端口,然后由中央服務(wù)器建立索引,進(jìn)而告知其他登錄的計(jì)算機(jī)。登錄后的各臺(tái)計(jì)算機(jī)根據(jù)中央服務(wù)器提供的信息,并在中央服務(wù)器的幫助下與其他計(jì)算機(jī)建立P2P連接,實(shí)現(xiàn)數(shù)據(jù)共享或通信。每臺(tái)計(jì)算機(jī)的連接和斷開(kāi)都要通過(guò)中央服務(wù)器通知所有登錄的計(jì)算機(jī)。這種架構(gòu)的優(yōu)點(diǎn)是實(shí)現(xiàn)了文件查詢和文件傳輸?shù)姆蛛x,有效地節(jié)省了中央服務(wù)器的帶寬消耗,減少了系統(tǒng)的文件傳輸延時(shí);缺點(diǎn)是增加了對(duì)服務(wù)器的依賴性,中央服務(wù)器的癱瘓容易導(dǎo)致整個(gè)網(wǎng)絡(luò)的崩潰。

P2P技術(shù)實(shí)現(xiàn)必須解決一大難題,就是NAT穿越的問(wèn)題。當(dāng)前許多計(jì)算機(jī)都隱藏在私有網(wǎng)絡(luò)中,通常因安全原因,外網(wǎng)的計(jì)算機(jī)無(wú)法知道內(nèi)網(wǎng)計(jì)算機(jī)上的P2P進(jìn)程編號(hào)(含IP地址和端口號(hào),見(jiàn)1.3.2節(jié)),更不允許直接連接內(nèi)網(wǎng)的計(jì)算機(jī)。由前面介紹的P2P原理可知,這對(duì)P2P的實(shí)現(xiàn)是致命的。因此必須解決穿越進(jìn)入私有網(wǎng)絡(luò)的問(wèn)題,其關(guān)鍵在于外部網(wǎng)絡(luò)進(jìn)程如何穿越網(wǎng)絡(luò)地址轉(zhuǎn)換(NetworkAddressTranslators,NAT)設(shè)備,與內(nèi)網(wǎng)計(jì)算機(jī)上的進(jìn)程建立全相關(guān)。11.2NAT穿越11.2.1NAT概念

NAT是在IP地址日益缺乏的情況下產(chǎn)生的,它的主要目的就是為了能夠地址重用,其產(chǎn)生基于如下事實(shí):一個(gè)私有網(wǎng)絡(luò)(域)中的節(jié)點(diǎn)中只有很少的節(jié)點(diǎn)需要與外網(wǎng)連接,那么這個(gè)子網(wǎng)中其實(shí)只有少數(shù)的節(jié)點(diǎn)需要全球唯一的IP地址,其他節(jié)點(diǎn)的IP地址應(yīng)該是可以重用的。

1.原理

NAT的核心功能就是進(jìn)行地址轉(zhuǎn)換。通常NAT設(shè)備有兩個(gè)NIC,一個(gè)接入Internet,一個(gè)接入LAN(因此其擁有兩個(gè)IP地址)。進(jìn)行NAT轉(zhuǎn)換可以分為數(shù)據(jù)向外和數(shù)據(jù)向內(nèi)兩種情況。一方面通過(guò)正確的地址轉(zhuǎn)換保障從私有內(nèi)網(wǎng)(簡(jiǎn)稱內(nèi)網(wǎng))傳出的數(shù)據(jù)可以路由到達(dá)外網(wǎng)終端上的網(wǎng)絡(luò)進(jìn)程,另一方面還要保障從外網(wǎng)終端上的網(wǎng)絡(luò)進(jìn)程返回的數(shù)據(jù)可以安全通過(guò)NAT到達(dá)內(nèi)網(wǎng)終端上的網(wǎng)絡(luò)進(jìn)程,其轉(zhuǎn)換過(guò)程如圖11-2所示。圖11-2NAT轉(zhuǎn)換過(guò)程為了對(duì)各種地址加以區(qū)分,通常使用以下四種地址進(jìn)行區(qū)分。

內(nèi)部本地地址(InsideLocalIPaddress,IL)是指內(nèi)網(wǎng)主機(jī)地址,圖11-2中主機(jī)Tom的IL地址就是,Jerry的IL地址就是。IL地址在外部網(wǎng)絡(luò)中是無(wú)法路由的或路由會(huì)將數(shù)據(jù)導(dǎo)向錯(cuò)誤的主機(jī)。

內(nèi)部全局地址(InsideGlobalIPaddress,IG)代表內(nèi)部IP到外部網(wǎng)絡(luò)可路由的合法IP,圖11-2中NAT的IG地址就是1,這是從外部看子網(wǎng),整個(gè)子網(wǎng)所呈現(xiàn)的地址,因此它代表整個(gè)內(nèi)向的地址。外部本地地址(OutsideLocalIPaddress,OL)是內(nèi)網(wǎng)主機(jī)所知的一臺(tái)連接外部網(wǎng)絡(luò)的主機(jī)IP,如圖11-2中NAT的OL地址就是8,這是從內(nèi)網(wǎng)主機(jī)向外網(wǎng)看去,整個(gè)外網(wǎng)所呈現(xiàn)的地址,因此它代表整個(gè)外圍的地址。

外部全局地址(OutsideGlobalIPaddress,OG)是外部網(wǎng)絡(luò)主機(jī)的合法IP,如圖11-2中所示的服務(wù)器地址為1。以圖11-2中的網(wǎng)絡(luò)結(jié)構(gòu)為例,進(jìn)行NAT地址轉(zhuǎn)換的兩種情況描述如下:

(1)數(shù)據(jù)傳出:假設(shè)終端Tom(IL地址為)上網(wǎng)絡(luò)進(jìn)程(端口號(hào)為1234)的一個(gè)數(shù)據(jù)包要透過(guò)NAT傳出到外網(wǎng)WWW服務(wù)器(OG地址為1)的Web服務(wù)程序上(端口號(hào)為80)。在進(jìn)行轉(zhuǎn)換之前,NAT設(shè)備查找路由并驗(yàn)證數(shù)據(jù)包是否合乎規(guī)則。如通過(guò)驗(yàn)證,則將該數(shù)據(jù)包的源(Sur)地址()替換為NAT的IG地址(1),再隨機(jī)安排一個(gè)NAT的端口號(hào)(如1222)替換Tom的端口號(hào)1234,記錄該對(duì)應(yīng)關(guān)系到地址轉(zhuǎn)換表中(如表11-1中的記錄1),從而這張轉(zhuǎn)換表就將這臺(tái)計(jì)算機(jī)的不可路由的IP地址及其端口號(hào)與路由器(這里是NAT)的IP地址綁定起來(lái)了。接下來(lái),就可以發(fā)送數(shù)據(jù)包到外網(wǎng),等待響應(yīng)。表11-1NAT地址轉(zhuǎn)換表

(2)數(shù)據(jù)傳入:當(dāng)一個(gè)數(shù)據(jù)包從目的服務(wù)器(1)發(fā)送回來(lái)時(shí),假設(shè)它的目的(Des)主機(jī)和端口號(hào)組合為1:1222,根據(jù)NAT地址轉(zhuǎn)換表的記錄1就可以確定目的計(jì)算機(jī)的IL地址和端口號(hào),其組合為:1234,則替換該數(shù)據(jù)包的Des地址和端口號(hào),然后發(fā)送至那臺(tái)計(jì)算機(jī)??梢?jiàn),NAT地址表中是否有對(duì)應(yīng)的記錄是外網(wǎng)數(shù)據(jù)能否進(jìn)入內(nèi)網(wǎng)的關(guān)鍵,利用地址轉(zhuǎn)換的工作原理可以實(shí)現(xiàn)NAT的穿越。

注意,在進(jìn)行IP地址和端口號(hào)替換后,需要重新計(jì)算數(shù)據(jù)包頭校驗(yàn)和,否則數(shù)據(jù)包會(huì)被丟棄。

2.分類

NAT的分類方法很多,按照發(fā)展過(guò)程可分為基本NAT和NAPT(NetworkAddress/PortTranslator)兩類。

最開(kāi)始,NAT是運(yùn)行在路由器上的一個(gè)功能模塊,并且首先出現(xiàn)的是基本NAT?;綨AT實(shí)現(xiàn)的功能很簡(jiǎn)單。在子網(wǎng)內(nèi)使用一個(gè)保留的IP子網(wǎng)段,這些IP對(duì)外是不可見(jiàn)的,而子網(wǎng)內(nèi)只有少數(shù)一些IP地址可以對(duì)應(yīng)到真正全球唯一的IP地址。如果這些節(jié)點(diǎn)需要訪問(wèn)外部網(wǎng)絡(luò),那么基本NAT就負(fù)責(zé)將這個(gè)節(jié)點(diǎn)的子網(wǎng)內(nèi)IP轉(zhuǎn)化為一個(gè)全球唯一的IP并發(fā)送出去(基本的NAT會(huì)改變IP包中的原IP地址,但是不會(huì)改變IP包中的端口)。

NAPT比NAT稍微復(fù)雜一些,它不但會(huì)改變經(jīng)過(guò)這個(gè)NAT設(shè)備的IP數(shù)據(jù)包的IP地址,還會(huì)改變IP數(shù)據(jù)包的TCP/UDP端口。目前NAPT是主流的NAT技術(shù),前面對(duì)圖11-2的說(shuō)明也是基于NAPT的。

3.轉(zhuǎn)發(fā)

顯然,根據(jù)前面對(duì)NAT工作原理的介紹,當(dāng)?shù)刂忿D(zhuǎn)換表中不存在一個(gè)外網(wǎng)的地址時(shí),則以這個(gè)地址為Des地址的數(shù)據(jù)包將會(huì)被丟棄,這就給P2P技術(shù)帶來(lái)致命問(wèn)題。如圖11-3所示的結(jié)構(gòu)中,客戶A想要直接建立與客戶B的連接就會(huì)因?yàn)镹AT的阻隔而失敗。

為了解決這一問(wèn)題,可以采用NAT轉(zhuǎn)發(fā)的方法加以解決。也就是客戶A、B以服務(wù)器S為中間人,轉(zhuǎn)發(fā)數(shù)據(jù)。A與S、B與S分別建立會(huì)話,由S對(duì)接收到的數(shù)據(jù)進(jìn)行地址替換,實(shí)現(xiàn)間接的A、B之間的通信,如圖11-3所示。顯然這樣會(huì)大大增加S的負(fù)擔(dān),在實(shí)際的網(wǎng)絡(luò)中幾乎不可行。

圖11-3NAT轉(zhuǎn)發(fā)

4.反向連接

還有一種解決外網(wǎng)計(jì)算機(jī)與內(nèi)網(wǎng)計(jì)算機(jī)的連接方法就是反向連接。這種方法的應(yīng)用條件比較特殊,即兩臺(tái)需要連接的計(jì)算機(jī)只有一臺(tái)在NAT后面,如圖11-4所示。

圖11-4NAT反向連接進(jìn)行反向連接的過(guò)程分以下三步:

(1)客戶B向中央服務(wù)器S發(fā)送與客戶A的連接請(qǐng)求;

(2)服務(wù)器S中繼該連接請(qǐng)求到客戶A;

(3)客戶A發(fā)起連接,與客戶B建立連接。

反向連接利用服務(wù)器S與客戶A之間已經(jīng)建立的合法通信(地址轉(zhuǎn)換關(guān)系已經(jīng)在客戶A登錄時(shí)就已經(jīng)插入NAT的地址轉(zhuǎn)換表),以及客戶A可以自由連接一臺(tái)具有OG地址的主機(jī)來(lái)實(shí)現(xiàn)P2P。但是由于反向連接的條件過(guò)于特殊,因此使用不是非常廣泛。

5.NAT穿越

目前解決NAT穿越主要是巧妙地利用TCP/IP的TCP和UDP協(xié)議,使用稱為“打洞”(holepunching)的技術(shù)。具體方法是:應(yīng)用程序向NAT外的服務(wù)器發(fā)送請(qǐng)求連接其他應(yīng)用程序的消息,收到請(qǐng)求消息后產(chǎn)生響應(yīng)消息,通知連接方和被連接方對(duì)方的IL、IG地址及對(duì)應(yīng)端口信息。利用得到的地址和端口信息,UDP和TCP各自采用不同的方法在NAT上建立地址轉(zhuǎn)換記錄,從而實(shí)現(xiàn)對(duì)方連接的數(shù)據(jù)包可以穿越NAT而進(jìn)入內(nèi)網(wǎng)。這時(shí)就可以建立不同NAT后計(jì)算機(jī)上應(yīng)用程序的連接了。

UDP打洞(見(jiàn)11.2.2節(jié))與TCP打洞(見(jiàn)11.2.3節(jié))稍有區(qū)別。上述這種NAT穿越方法最大的優(yōu)點(diǎn)是無(wú)需現(xiàn)有NAT設(shè)備做任何改動(dòng),而且可以適用于多級(jí)NAT的網(wǎng)絡(luò)結(jié)構(gòu),是當(dāng)前NAT穿越采用的主流技術(shù)。11.2.2UDP打洞

UDP打洞利用UPD協(xié)議,在中央服務(wù)器的協(xié)調(diào)下實(shí)現(xiàn)NAT穿越。

假設(shè)在如圖11-5所示的網(wǎng)絡(luò)結(jié)構(gòu)上實(shí)現(xiàn)UDP穿越。這個(gè)結(jié)構(gòu)中存在兩臺(tái)計(jì)算機(jī)客戶A和客戶B,它們都擁有自己的私有IP地址,并且都處在不同的NAT之后。相同的P2P程序分別運(yùn)行于A和B上。在公網(wǎng)上存在一臺(tái)中央服務(wù)器S,并且對(duì)它們都開(kāi)放了UDP端口1234。A和B首先分別與S建立通信會(huì)話,這時(shí)NATA把它自己的UDP端口62000分配給A與S的會(huì)話,NATB也把自己的UDP端口31000分配給B與S的會(huì)話。在建立通話的過(guò)程中,S通過(guò)數(shù)據(jù)包的源地址和內(nèi)容已經(jīng)了解了A和B的IL地址和各自的NATIG地址及對(duì)應(yīng)的端口號(hào)。

圖11-5UDP打洞過(guò)程

A希望與B建立端對(duì)端的連接,其UDP打洞分為以下三步:

(1)

A開(kāi)始發(fā)送一個(gè)UDP信息到S,并提出連接B的請(qǐng)求;

(2)S收到請(qǐng)求后,向A和B分別發(fā)出對(duì)方的IL地址和各自的NATIG地址以及對(duì)應(yīng)的端口號(hào),即A得到B的IL地址及端口是:4321,B的IG地址及端口是:31000;B得到A的IL地址及端口是:4321,A的IG地址及端口是1:62000。

(3)在得到對(duì)方的地址后,A和B發(fā)出兩個(gè)UDP連接,一個(gè)連接對(duì)方的IG地址和端口號(hào),一個(gè)直接指向?qū)Ψ降腎L地址和端口號(hào)的連接。這時(shí)A向B的IG地址(:31000)發(fā)送的信息導(dǎo)致NATA增加一條1:62000與:31000的地址轉(zhuǎn)換記錄,同樣,B向A的IG地址(1:62000)發(fā)送的信息導(dǎo)致NATA增加一條:31000與1:62000的地址轉(zhuǎn)換記錄。此時(shí)會(huì)有兩種情況發(fā)生:一種是A連接B的IG地址和端口號(hào)先于B連接A的IG地址和端口號(hào)到達(dá)NATB,但由于NATB上的地址轉(zhuǎn)換記錄還沒(méi)有建立,因此A連接B的IG地址數(shù)據(jù)包會(huì)被丟棄;另一種是A連接B的IG地址和端口號(hào)晚于B連接A的IG地址和端口號(hào)到達(dá)NATB,這時(shí)NATB上的地址轉(zhuǎn)換記錄已經(jīng)建立,因此A連接B的IG地址數(shù)據(jù)包會(huì)被轉(zhuǎn)發(fā)給B(依據(jù)地址映射上的對(duì)應(yīng)關(guān)系)。無(wú)論哪一種情況發(fā)生,最終總是有一個(gè)NAT被穿越,那么A和B之間就可以直接進(jìn)行通信而無(wú)需S的協(xié)助了。

之所以在步驟(3)中還要發(fā)送一個(gè)直接指向?qū)Ψ降腎L地址和端口號(hào)的連接,就是考慮到A與B同處于一個(gè)NAT,則這個(gè)連接一定會(huì)比由NAT“發(fā)卡”式轉(zhuǎn)發(fā)要有更高的效率。11.2.3TCP打洞

TCP打洞利用TCP協(xié)議,在中央服務(wù)器的協(xié)調(diào)下實(shí)現(xiàn)NAT穿越。同樣采用11.2.2節(jié)的網(wǎng)絡(luò)結(jié)構(gòu)實(shí)現(xiàn)TCP打洞,A希望與B建立端對(duì)端的連接,分為三個(gè)步驟,如圖11-6所示。圖11-6TCP打洞過(guò)程

(1)客戶A和B分別登錄服務(wù)器S,A可以通過(guò)S得到B的IL和IG地址,B也可以通過(guò)S得到A的IL和IG地址,此時(shí)A與B相互發(fā)送的消息會(huì)被NAT所丟棄。

(2)為了完成TCP打洞,穿越NAT,A會(huì)打開(kāi)一個(gè)偵聽(tīng)套接字,接收來(lái)自B的信息,再打開(kāi)一個(gè)套接字并向B的IL地址發(fā)一個(gè)SYN包(兩個(gè)套接字使用端口復(fù)用);B打開(kāi)一個(gè)偵聽(tīng)套接字,接收來(lái)自A的信息,也可以再打開(kāi)一個(gè)套接字向A的公網(wǎng)地址發(fā)送SYN包(兩個(gè)套接字同樣使用端口復(fù)用)。

(3)雙方經(jīng)過(guò)三次握手(見(jiàn)6.3.4節(jié))建立TCP直連。

在TCP打洞的過(guò)程中可能因操作系統(tǒng)的工作機(jī)制不同而導(dǎo)致截然不同的連接實(shí)現(xiàn)。

假設(shè)A的第一個(gè)SYN包到B的公網(wǎng)地址后被B的NAT丟棄,但是B的第一個(gè)SYN包到A的公網(wǎng)地址后通過(guò)A,在A再次發(fā)送一個(gè)SYN包,根據(jù)操作系統(tǒng)的差異可能發(fā)生以下兩種情況。

情況一:A連接B的套接字注意到這個(gè)到達(dá)的SYN包與剛發(fā)向B的SYN包所對(duì)應(yīng)的會(huì)話匹配。A的TCP棧因此聯(lián)系這個(gè)SYN包到剛剛A試圖連接到B的公共地址的那個(gè)套接字上。因此Connect同步成功,而偵聽(tīng)的那個(gè)套接字上什么也沒(méi)有發(fā)生。由于收到的SYN包不包含A先前發(fā)送的SYN的ACK,A的TCP棧重發(fā)給B的公共地址一個(gè)帶SYN-ACK的包,ISN部分只是原來(lái)ISN的重復(fù)(使用相同的序號(hào)),一旦B收到A的SYN-ACK包,它會(huì)響應(yīng)A的SYN包的ACK,則TCP會(huì)話完成三次握手而進(jìn)入連接狀態(tài)。

情況二:與情況一不同,A的活動(dòng)狀態(tài)的Listen套接字注意到B發(fā)出的SYN包,由于該包看起來(lái)像是一個(gè)對(duì)A的連接嘗試,A的TCP棧會(huì)使用Accept()函數(shù),返回值為這個(gè)新的TCP會(huì)話產(chǎn)生一個(gè)新的流套接字。與情況一類似,A用一個(gè)SYN-ACK包進(jìn)行響應(yīng),則這個(gè)TCP連接的啟動(dòng)過(guò)程就像是一個(gè)平常的C/S風(fēng)格,TCP連接順利建立。由于A先前對(duì)B的connect()使用了當(dāng)前的源地址到目的地址的組合,而這個(gè)組合又被上面的套接字再次使用,則A的這個(gè)Connect()必定失敗,產(chǎn)生一個(gè)典型的“地址已使用”錯(cuò)誤。但是,應(yīng)用仍然會(huì)工作在前面已建立連接的流套接字,這個(gè)錯(cuò)誤會(huì)被忽略,TCP連接還是被建立起來(lái)了。

前一種情況通常出現(xiàn)在基于BSD的操作系統(tǒng),后一種多出現(xiàn)在Linux和Windows操作系統(tǒng)上。

如前所述,TCP打洞的一個(gè)重要條件就是要將兩個(gè)不同的套接字綁定到一個(gè)端口上,因此必須采用端口復(fù)用技術(shù)。由于傳統(tǒng)的標(biāo)準(zhǔn)伯克利(Berkeley)網(wǎng)卡要么用來(lái)連接主動(dòng)建立對(duì)外連接,要么使用Listen()和Accept()被動(dòng)建立來(lái)自外部的連接,不能提供類似UDP那樣的同一端口既可以向外連接又能接受來(lái)自外部的連接,即僅允許建立一對(duì)一的響應(yīng),應(yīng)用程序在將一個(gè)套接字綁定到本地一個(gè)端口以后,任何試圖將第二個(gè)套接字綁定到該端口的操作都會(huì)失敗。而為了實(shí)現(xiàn)TCP打洞就必須使用一個(gè)本地TCP端口來(lái)監(jiān)聽(tīng)來(lái)自外部的TCP連接,同時(shí)建立多個(gè)向外連接的TCP連接,這就需要使用setsockopt()函數(shù)進(jìn)行端口復(fù)用設(shè)置,實(shí)現(xiàn)代碼如下:

Boolval;

Socketsock;

Setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,(char*)&val,sizeof(val));

Winsock2對(duì)端口復(fù)用技術(shù)提供很好的支持,這也是其特色之一。

基于混合型P2P架構(gòu)和UDP協(xié)議的基本P2P連接功能,總體軟件可分為P2P協(xié)議、服務(wù)端和客戶端三部分(這與傳統(tǒng)的C/S意義完全不同)。程序設(shè)計(jì)前有如下約定:11.3P2P編程

①P2PServer運(yùn)行在一個(gè)擁有公網(wǎng)IP的計(jì)算機(jī)上,P2PClient運(yùn)行在兩個(gè)不同的NAT后;②后登錄的計(jì)算機(jī)可以獲得先登錄計(jì)算機(jī)的用戶名,后登錄的計(jì)算機(jī)通過(guò)“sendusernamemessage”的格式來(lái)發(fā)送消息,如果發(fā)送成功,說(shuō)明已取得了直接與對(duì)方連接的成功;③程序使用了三個(gè)命令,即send、getu、exit,其中,send代表發(fā)送信息給用戶,getu代表獲得當(dāng)前服務(wù)器用戶列表,exit代表客戶端注銷與服務(wù)器的連接。11.3.1P2P協(xié)議程序

P2P軟件必須商定統(tǒng)一的協(xié)議,否則各個(gè)端點(diǎn)將無(wú)法協(xié)調(diào)。協(xié)議需要規(guī)定命令、信息協(xié)議、消息類型、消息格式。本節(jié)協(xié)議將這些信息分為客戶端與服務(wù)器間的通信信息協(xié)議和客戶端間的通信信息協(xié)議兩類實(shí)現(xiàn)。

//protocol.h

#pragmaonce

#include<list>

//定義iMessageType的值

#defineLOGIN 1

#defineLOGOUT 2

#defineP2PTRANS 3

#defineGETALLUSER 4

#defineSERVER_PORT60000 //服務(wù)器端口

//======================================

//客戶端與服務(wù)器間通信信息協(xié)議

//======================================

structstLoginMessage //客戶端登錄服務(wù)器發(fā)送的消息

{

charuserName[10];

charpassword[10];

};

structstLogoutMessage //客戶端注銷時(shí)發(fā)送的消息

{

charuserName[10];

};

structstP2PTranslate //客戶端向服務(wù)器請(qǐng)求另外一個(gè)Client向UDP發(fā)送打洞消息

{

charuserName[10];

};

structstMessage //客戶端向服務(wù)器發(fā)送的消息格式

{

intiMessageType;

union_message

{

stLoginMessageloginmember;

stLogoutMessagelogoutmember;

stP2PTranslatetranslatemessage;

}message;

};

structstUserListNode //客戶節(jié)點(diǎn)信息

{

charuserName[10];

unsignedintip;

unsignedshortport;

};

structstServerToClient //服務(wù)端向客戶端發(fā)送的消息

{

intiMessageType;

union_message

{

stUserListNodeuser;

}message;

};

//======================================

//客戶端間通信信息協(xié)議

//======================================

#defineP2PMESSAGE 100 //發(fā)送消息

#defineP2PMESSAGEACK 101 //收到消息的應(yīng)答

#defineP2PSOMEONEWANTTOCALLYOU 102 //服務(wù)器向客戶端發(fā)送的消息,希望它發(fā)送

//UDP打洞包

#defineP2PTRASH 103 //客戶端發(fā)送的打洞包,接收端應(yīng)該

//忽略此消息

structstP2PMessage //客戶端之間發(fā)送消息格式

{

intiMessageType; //信息類型

intiStringLen; //地址信息

unsignedshortPort; //端口信息

};

usingnamespacestd;

typedeflist<stUserListNode*>UserList;

在上述協(xié)議文件中,使用了UserList來(lái)記錄用戶的信息??蛻舳伺c服務(wù)器端均需遵循上述協(xié)議,具體方法是通過(guò)#include"protocol.h"引入這個(gè)協(xié)議頭文件,并在通信過(guò)程中遵循它規(guī)定的信息格式來(lái)實(shí)現(xiàn)。11.3.2服務(wù)器端程序

服務(wù)器端程序主要完成的任務(wù)包括三項(xiàng):檢測(cè)在線用戶情況、維護(hù)用戶列表、轉(zhuǎn)達(dá)客戶方打洞的請(qǐng)求。主要工作由main()函數(shù)的一個(gè)for循環(huán)完成,分別對(duì)四種客戶請(qǐng)求進(jìn)行不同的響應(yīng)。

//P2PServer.cpp

#include"windows.h"

#include"protocol.h"

#include“winsock2.h”

#pragmacomment(lib,"ws2_32.lib")

UserListClientList;

DWORDStartSock(){//同6.3.2節(jié),略}

stUserListNodeGetUser(char*username)//檢索所需用戶信息

{

for(UserList::iteratorUserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator)

if(strcmp(((*UserIterator)->userName),username)==0)return*(*UserIterator);

stUserListNodeNULLnode={"absent",0,0};//如果未找到則返回一個(gè)空節(jié)點(diǎn)信息

returnNULLnode;

}

intmain(intargc,char*argv[])

{

StartSock();

SOCKETPrimaryUDP;

PrimaryUDP=socket(AF_INET,SOCK_DGRAM,0);

sockaddr_inlocal;

local.sin_family=AF_INET;

local.sin_port=htons(SERVER_PORT);

local.sin_addr.s_addr=htonl(INADDR_ANY);

intnResult=bind(PrimaryUDP,(sockaddr*)&local,sizeof(sockaddr));

if(nResult==SOCKET_ERROR)

{

printf("binderror!\n");

return0;

}

sockaddr_insender;

stMessagerecvbuf;

memset(&recvbuf,0,sizeof(stMessage));

for(;;)//主循環(huán)

{

intdwSender=sizeof(sockaddr_in);

intret=recvfrom(PrimaryUDP,(char*)&recvbuf,sizeof(stMessage),0,

(sockaddr*)&sender,&dwSender);

if(ret<=0)

{

printf("recverror");

continue;

}

else

{

intmessageType=recvbuf.iMessageType;

switch(messageType){

caseLOGIN: //將這個(gè)用戶的信息記錄到用戶列表中

{

printf("hasauserlogin:%s\n",recvbuf.message.loginmember.userName);

stUserListNode*currentuser=newstUserListNode();

strcpy(currentuser->userName,recvbuf.message.loginmember.userName);

currentuser->ip=ntohl(sender.sin_addr.S_un.S_addr);

currentuser->port=ntohs(sender.sin_port); ClientList.push_back(currentuser);

//發(fā)送已經(jīng)登錄的客戶信息

intnodecount=(int)ClientList.size();

sendto(PrimaryUDP,(constchar*)&nodecount,sizeof(int),0,

(constsockaddr*)&sender,sizeof(sender));

for(UserList::iteratorUserIterator=ClientList.begin();

UserIterator!=ClientList.end();++UserIterator)

{

sendto(PrimaryUDP,(constchar*)(*UserIterator),sizeof(stUserListNode),

0,(constsockaddr*)&sender,sizeof(sender));

}

break;

}

caseLOGOUT: //將此客戶信息刪除

{

printf("hasauserlogout:%s\n",recvbuf.message.logoutmember.userName);

UserList::iteratorremoveiterator=NULL;

for(UserList::iteratorUserIterator=ClientList.begin();

UserIterator!=ClientList.end();++UserIterator)

{ if(strcmp(((*UserIterator)->userName),

recvbuf.message.logoutmember.userName)==0)

{

removeiterator=UserIterator;

break;

}

}

if(removeiterator!=NULL)ClientList.remove(*removeiterator);

break;

}

caseP2PTRANS://某個(gè)客戶希望服務(wù)端向另外一個(gè)客戶發(fā)送一個(gè)打洞消息

{

printf("%swantstop2p%s\n",inet_ntoa(sender.sin_addr),

recvbuf.message.translatemessage.userName);

stUserListNodenode=GetUser(recvbuf.message.translatemessage.userName);

sockaddr_inremote;

remote.sin_family=AF_INET;

remote.sin_port=htons(node.port);

remote.sin_addr.s_addr=htonl(node.ip);

in_addrtmp;

tmp.S_un.S_addr=htonl(node.ip);

printf("theaddressis%s,andportis%d\n",inet_ntoa(tmp),node.port);

stP2PMessagetransMessage;

transMessage.iMessageType=P2PSOMEONEWANTTOCALLYOU;

transMessage.iStringLen=ntohl(sender.sin_addr.S_un.S_addr);

transMessage.Port=ntohs(sender.sin_port);

sendto(PrimaryUDP,(constchar*)&transMessage,sizeof(transMessage),0,

(constsockaddr*)&remote,sizeof(remote));

break;

}

caseGETALLUSER: //獲取服務(wù)器上所有用戶的信息

{

intcommand=GETALLUSER;

sendto(PrimaryUDP,(constchar*)&command,sizeof(int),0,

(constsockaddr*)&sender,sizeof(sender));

intnodecount=(int)ClientList.size();

sendto(PrimaryUDP,(constchar*)&nodecount,sizeof(int),0,

(constsockaddr*)&sender,sizeof(sender));

for(UserList::iteratorUserIterator=ClientList.begin();

UserIterator!=ClientList.end();++UserIterator)

{

sendto(PrimaryUDP,(constchar*)(*UserIterator),sizeof(stUserListNode),

0,(constsockaddr*)&sender,sizeof(sender));

}

break;

}

}

}

}

return0;

}

程序中的GetUser()函數(shù)通過(guò)字符串比較來(lái)檢索已經(jīng)登錄的用戶。11.3.3客戶端程序

客戶端程序完成P2P連接的工作過(guò)程是:首先登錄服務(wù)器,獲得已經(jīng)登錄服務(wù)器的用戶列表,然后選擇一個(gè)用戶對(duì)其發(fā)送消息,之后根據(jù)用戶的指令進(jìn)行其他操作。發(fā)送消息給某個(gè)用戶的流程是:直接向某個(gè)用戶的外網(wǎng)IP發(fā)送消息,如果此前沒(méi)有聯(lián)系過(guò),那么此消息將無(wú)法發(fā)送,發(fā)送端等待超時(shí);超時(shí)后,發(fā)送端將發(fā)送一個(gè)請(qǐng)求信息到服務(wù)端,要求服務(wù)端發(fā)送給該客戶端一個(gè)請(qǐng)求,請(qǐng)求它給本機(jī)發(fā)送打洞消息,從而實(shí)現(xiàn)NAT的打洞。

//P2PClient.cpp

#include"windows.h"

#include"protocol.h"

#include<iostream>

#include<winsock2.h>

usingnamespacestd;

#pragmacomment(lib,"ws2_32.lib")

#defineCOMMANDMAXC256

#defineMAXRETRY5

UserListClientList;

SOCKETPrimaryUDP;

charUserName[10];

charServerIP[20];

boolRecvedACK;

DWORDStartSock(){//同6.3.2節(jié),略}

stUserListNodeGetUser(char*username)

{

for(UserList::iteratorUserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator)

if(strcmp(((*UserIterator)->userName),username)==0)return*(*UserIterator);

stUserListNodeNULLnode={"absent",0,0};//如果未找到則返回一個(gè)空節(jié)點(diǎn)信息

returnNULLnode;

}

voidConnectToServer(SOCKETsock,char*username,char*serverip)

{

sockaddr_inremote;

remote.sin_addr.S_un.S_addr=inet_addr(serverip);

remote.sin_family=AF_INET;

remote.sin_port=htons(SERVER_PORT);

stMessagesendbuf;

sendbuf.iMessageType=LOGIN;

strncpy(sendbuf.message.loginmember.userName,username,10);

sendto(sock,(constchar*)&sendbuf,sizeof(sendbuf),0,(constsockaddr*)&remote,sizeof(remote));

intusercount;

intfromlen=sizeof(remote);

intiread=recvfrom(sock,(char*)&usercount,sizeof(int),0,(sockaddr*)&remote,&fromlen);

if(iread<=0){printf("binderror!\n");}

//登錄到服務(wù)器端后,接收服務(wù)器端發(fā)送來(lái)的已經(jīng)登錄的用戶的信息

cout<<"Have"<<usercount<<"usersloginedserver:"<<endl;

for(inti=0;i<usercount;i++)

{

stUserListNode*node=newstUserListNode;

recvfrom(sock,(char*)node,sizeof(stUserListNode),0,(sockaddr*)&remote,&fromlen);

ClientList.push_back(node);

cout<<"Username:"<<node->userName<<endl;

in_addrtmp;

tmp.S_un.S_addr=htonl(node->ip);

cout<<"UserIP:"<<inet_ntoa(tmp)<<endl;

cout<<"UserPort:"<<node->port<<endl;

cout<<""<<endl;

}

}

voidOutputUsage()//屏幕打印使用方法簡(jiǎn)介

{

cout<<"Youcaninputyoucommand:\n"<<"CommandType:\"send\",\"exit\",\"getu\"\n"

<<"Example:sendUsernameMessage\n"<<"exit\n"<<"getu\n"<<endl;

}

boolSendMessageTo(char*UserName,char*Message)

{

charrealmessage[256];

unsignedintUserIP;

unsignedshortUserPort;

boolFindUser=false;

for(UserList::iteratorUserIterator=ClientList.begin();UserIterator!=ClientList.end();++UserIterator)

{

if(strcmp(((*UserIterator)->userName),UserName)==0)

{

UserIP=(*UserIterator)->ip;

UserPort=(*UserIterator)->port;

FindUser=true;

}

}

if(!FindUser)returnfalse;

strcpy(realmessage,Message);

for(inti=0;i<MAXRETRY;i++)

{

RecvedACK=false;

sockaddr_inremote;

remote.sin_addr.S_un.S_addr=htonl(UserIP);

remote.sin_family=AF_INET;

remote.sin_port=htons(UserPort);

stP2PMessageMessageHead;

MessageHead.iMessageType=P2PMESSAGE;

MessageHead.iStringLen=(int)strlen(realmessage)+1;

intisend=sendto(PrimaryUDP,(constchar*)&MessageHead,sizeof(MessageHead),0,

(constsockaddr*)&remote,sizeof(remote));

isend=sendto(PrimaryUDP,(constchar*)&realmessage,MessageHead.iStringLen,0,

(constsockaddr*)&remote,sizeof(remote));

for(intj=0;j<10;j++) //等待接收線程時(shí)將此標(biāo)記修改

{

if(RecvedACK)returntrue;

elseSleep(300);

}

//若未接收到目標(biāo)主機(jī)回應(yīng),則目標(biāo)主機(jī)端口映射未打開(kāi),發(fā)送請(qǐng)求信息給服務(wù)器,

//要其告訴目標(biāo)主機(jī)打開(kāi)映射端口(UDP打洞)

sockaddr_inserver;

server.sin_addr.S_un.S_addr=inet_addr(ServerIP);

server.sin_family=AF_INET;

server.sin_port=htons(SERVER_PORT);

stMessagetransMessage;

transMessage.iMessageType=P2PTRANS;

strcpy(transMessage.message.translatemessage.userName,UserName);

sendto(PrimaryUDP,(constchar*)&transMessage,sizeof(transMessage),0,

(constsockaddr*)&server,sizeof(server));

Sleep(100);//等待對(duì)方先發(fā)送信息

}

returnfalse;

}

voidParseCommand(char*CommandLine) //解析命令獲取當(dāng)前服務(wù)器的所有用戶

{

if(strlen(CommandLine)<4)return;

charCommand[10];

strncpy(Command,CommandLine,4);

Command[4]='\0';

if(strcmp(Command,"exit")==0)

{

stMessagesendbuf;

sendbuf.iMessageType=LOGOUT;

strncpy(sendbuf.message.logoutmember.userName,UserName,10);

sockaddr_inserver;

server.sin_addr.S_un.S_addr=inet_addr(ServerIP);

server.sin_family=AF_INET;

server.sin_port=htons(SERVER_PORT);

sendto(PrimaryUDP,(constchar*)&sendbuf,sizeof(sendbuf),0,

(constsockaddr*)&server,sizeof(server));

shutdown(PrimaryUDP,2);

closesocket(PrimaryUDP);

//exit(0);亦可在此處結(jié)束進(jìn)程

}

elseif(strcmp(Command,"send")==0)

{

charsendname[20];

charmessage[COMMANDMAXC];

inti;

for(i=5;;i++)

{

if(CommandLine[i]!='') sendname[i-5]=CommandLine[i];

else

{

sendname[i-5]='\0';

break;

}

}

strcpy(message,&(CommandLine[i+1]));

if(SendMessageTo(sendname,message))printf("SendOK!\n");

elseprintf("SendFailure!\n");

}

elseif(strcmp(Command,"getu")==0)

{

intcommand=GETALLUSER;

sockaddr_inserver;

server.sin_addr.S_un.S_addr=inet_addr(ServerIP);

server.sin_family=AF_INET;

server.sin_port=htons(SERVER_PORT);

sendto(PrimaryUDP,(constchar*)&command,sizeof(command),0,

(constsockaddr*)&server,sizeof(server));

}

}

DWORDWINAPIRecvThreadProc(LPVOIDlpParameter) //接收消息線程

{

sockaddr_inremote;

intsinlen=sizeof(remote);

stP2PMessagerecvbuf;

for(;;)

{

intiread=recvfrom(PrimaryUDP,(char*)&recvbuf,sizeof(recvbuf),0,

(sockaddr*)&remote,&sinlen);

if(iread<=0)

{

printf("recverror\n");

continue;

}

switch(recvbuf.iMessageType)

{

caseP2PMESSAGE: //接收到P2P的消息

{

char*comemessage=newchar[recvbuf.iStringLen];

intiread1=recvfrom(PrimaryUDP,comemessage,256,0,(sockaddr*)

&remote,&sinlen);

comemessage[iread1-1]='\0';

if(iread1<=0)printf("receiveerror!\n");

else

{

printf("RecvaMessage:%s\n",comemessage);

stP2PMessagesendbuf;

sendbuf.iMessageType=P2PMESSAGEACK;

sendto(PrimaryUDP,(constchar*)&sendbuf,sizeof(sendbuf),0,

(constsockaddr*)&remote,sizeof(remote));

}

delete[]comemessage;

break;

}

caseP2PSOMEONEWANTTOCALLYOU://接收到打洞命令,向指定的IP地址打洞

{

printf("Recvp2someonewanttocallyoudata\n");

sockaddr_inremote;

remote.sin_addr.S_un.S_addr=htonl(recvbuf.iStringLen);

remote.sin_family=AF_INET;

remote.sin_port=htons(recvbuf.Port);

stP2PMessagemessage; //UDP打洞

message.iMessageType=P2PTRASH;

sendto(PrimaryUDP,(constchar*)&message,sizeof(message),0,

溫馨提示

  • 1. 本站所有資源如無(wú)特殊說(shuō)明,都需要本地電腦安裝OFFICE2007和PDF閱讀器。圖紙軟件為CAD,CAXA,PROE,UG,SolidWorks等.壓縮文件請(qǐng)下載最新的WinRAR軟件解壓。
  • 2. 本站的文檔不包含任何第三方提供的附件圖紙等,如果需要附件,請(qǐng)聯(lián)系上傳者。文件的所有權(quán)益歸上傳用戶所有。
  • 3. 本站RAR壓縮包中若帶圖紙,網(wǎng)頁(yè)內(nèi)容里面會(huì)有圖紙預(yù)覽,若沒(méi)有圖紙預(yù)覽就沒(méi)有圖紙。
  • 4. 未經(jīng)權(quán)益所有人同意不得將文件中的內(nèi)容挪作商業(yè)或盈利用途。
  • 5. 人人文庫(kù)網(wǎng)僅提供信息存儲(chǔ)空間,僅對(duì)用戶上傳內(nèi)容的表現(xiàn)方式做保護(hù)處理,對(duì)用戶上傳分享的文檔內(nèi)容本身不做任何修改或編輯,并不能對(duì)任何下載內(nèi)容負(fù)責(zé)。
  • 6. 下載文件中如有侵權(quán)或不適當(dāng)內(nèi)容,請(qǐng)與我們聯(lián)系,我們立即糾正。
  • 7. 本站不保證下載資源的準(zhǔn)確性、安全性和完整性, 同時(shí)也不承擔(dān)用戶因使用這些下載資源對(duì)自己和他人造成任何形式的傷害或損失。

評(píng)論

0/150

提交評(píng)論