版權(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ì)自己和他人造成任何形式的傷害或損失。
最新文檔
- 辦公樓租賃合同
- 民爆器材倉(cāng)庫(kù)管理制度(3篇)
- 教育統(tǒng)計(jì)工作管理制度范文(2篇)
- 養(yǎng)殖場(chǎng)防疫管理制度模版(2篇)
- 安檢機(jī)構(gòu)年度報(bào)告制度模版(三篇)
- 農(nóng)民工業(yè)余學(xué)校管理制度(4篇)
- 2024年煤礦隱患日常檢查制度(3篇)
- 2024年設(shè)備采購(gòu)供應(yīng)商的評(píng)審管理制度(2篇)
- 交通安全應(yīng)急預(yù)案范例(2篇)
- 2024年防火安全演講稿樣本(3篇)
- DB32T 4578.2-2023 丙型病毒性肝炎防治技術(shù)指南 第2部分:患者管理
- 廣東省茂名市崇文學(xué)校2023-2024學(xué)年九年級(jí)上學(xué)期期末英語(yǔ)試卷(無(wú)答案)
- 眼科專科題庫(kù)+答案
- 智能化安裝合同補(bǔ)充協(xié)議
- 英語(yǔ)期末復(fù)習(xí)講座模板
- 京東管理培訓(xùn)生
- 北京市西城區(qū)2023-2024學(xué)年六年級(jí)上學(xué)期語(yǔ)文期末試卷
- 市政苗木移植合同范例
- 化學(xué)與生活2023-2024-2學(xué)習(xí)通超星期末考試答案章節(jié)答案2024年
- 畜禽市場(chǎng)管理制度5則范文
- GB/T 30595-2024建筑保溫用擠塑聚苯板(XPS)系統(tǒng)材料
評(píng)論
0/150
提交評(píng)論