内网穿透实现P2P通信
P2P 通信最大的障礙就是 NAT(網絡地址轉換),NAT 使得局域網內的設備可以與公網進行通訊,但是不同 NAT 下的設備之間通訊將會變得很困難。UDP 打洞就是用來使得設備間繞過 NAT 進行通訊的一種技術。
一、背景知識介紹
1.什么是NAT?
NAT(Network Address Translation,網絡地址轉換)是一種網絡地址翻譯技術,主要是將內部的私有IP地址(private IP)轉換成可以在公網使用的公網IP(public IP)。
2.為什么會有NAT?
時光回到上個世紀80年代,當時的人們在設計網絡地址的時候,覺得再怎么樣也不會有超過32bit位長即2的32次冪臺終端設備連入互聯(lián)網,再加上增加ip的長度(即使是從4字節(jié)增到6字節(jié))對當時設備的計算、存儲、傳輸成本也是相當巨大的。后來逐漸發(fā)現(xiàn)IP地址不夠用了,然后就NAT就誕生了!(雖然ipv6也是解決辦法,但始終普及不開來,而且未來到底ipv6夠不夠用仍是未知)。
因此,NAT技術能夠興起的原因還是因為在我們國家公網IP地址太少了,不夠用,所以才會采取這種地址轉換的策略。可見,NAT的本質就是讓一群機器公用同一個IP,這樣就暫時解決了IP短缺的問題。
3.NAT有什么優(yōu)缺點?
優(yōu)勢其實上面已經剛剛討論過了,根據(jù)定義,比較容易看出,NAT可以同時讓多個計算機同時聯(lián)網,并隱藏其內網IP,因此也增加了內網的網絡安全性;此外,NAT對來自外部的數(shù)據(jù)查看其NAT映射記錄,對沒有相應記錄的數(shù)據(jù)包進行拒絕,提高了網絡安全性。
那么,NAT與此同時也帶來一些弊端:首先是,NAT設備會對數(shù)據(jù)包進行編輯修改,這樣就降低了發(fā)送數(shù)據(jù)的效率;此外,各種協(xié)議的應用各有不同,有的協(xié)議是無法通過NAT的(不能通過NAT的協(xié)議還是蠻多的),這就需要通過穿透技術來解決。我們后面會重點討論穿透技術。
二、NAT介紹
1.NAT工作原理:
首先,NAT A 網下的設備 1(192.168.1.101)想與某公網 IP 通訊,設備 1 將包發(fā)給 NAT A,然后 NAT A 對源 IP 進行轉換(123.122.53.20)發(fā)給 NAT B(中間可能還會經過多重 NAT)。
這樣做的目的是,NAT B 并不知曉 NAT A 下的各個設備,他只能與 NAT A 本身通訊,因此發(fā)送給 NAT B 的包源 IP 必須是 NAT A 的公網 IP,不然 NAT B 沒有辦法進行回復。
接下來 NAT B 將回復包再發(fā)回 NAT A,此時就是 NAT 發(fā)揮作用的時候了,NAT A 現(xiàn)在要做的就是將包再分發(fā)回之前的設備,如何確定要發(fā)給誰呢?NAT 中記錄了一張表,之前 192.168.1.101 通過 2333 端口與 42.120.241.46 端口 443 通訊了,并且 NAT A 是用 60001 的端口轉發(fā)出去的,那么這次接受到發(fā)往該 NAT 60001 端口的包時就應該再通過 2333 端口轉發(fā)給 192.168.1.101。經過這樣的過程,NAT A 下的設備都可以連接到互聯(lián)網了!
2.NAT特性:
1.網絡訪問只能先由私網側發(fā)起,公網無法主動訪問私網主機;
2.NAT網關在兩個訪問方向上完成兩次地址的轉換或翻譯,出方向做源信息替換,入方向做目的信息替換;
3.NAT網關的存在對通信雙方是保持透明的;
4.NAT網關為了實現(xiàn)雙向翻譯的功能,需要維護一張關聯(lián)表,把會話的信息保存下來。
3.NAT類型:
NAT分為基礎型NAT(靜態(tài)NAT即Static NAT,動態(tài)NAT即Dynamic NAT/Pooled NAT)和NAPT(Network Address Port Translation)兩種,但由于基礎型NAT已不常用,我們通常提到的NAT就代指NAPT。NAPT是指網絡地址轉換過程中使用了端口復用技術,即PAT(Port address Translation)。
對于基本使用的NAPT,又分為對稱和錐型NAT。
1.錐型NAT,有完全錐型、受限制錐型、端口受限制錐型三種:
a) Full Cone NAT(完全圓錐型):
特點:IP和端口都不受限。
表現(xiàn)形式:從同一私網地址端口192.168.0.8:4000發(fā)至公網的所有請求都映射成同一個公網地址端口1.2.3.4:62000 ,192.168.0.8可以收到任意外部主機發(fā)到1.2.3.4:62000的數(shù)據(jù)報。
b) Restricted Cone NAT (限制圓錐型):
特點:IP受限,端口不受限。
表現(xiàn)形式: 從同一私網地址端口192.168.0.8:4000發(fā)至公網的所有請求都映射成同一個公網地址端口1.2.3.4:62000,只有當內部主機192.168.0.8先給服務器C 6.7.8.9發(fā)送一個數(shù)據(jù)報后,192.168.0.8才能收到6.7.8.9發(fā)送到1.2.3.4:62000的數(shù)據(jù)報。
c)Port Restricted Cone NAT(端口限制圓錐型):
特點:IP和端口都受限。
表現(xiàn)形式:從同一私網地址端口192.168.0.8:4000發(fā)至公網的所有請求都映射成同一個公網地址端口1.2.3.4:62000,只有當內部主機192.168.0.8先向外部主機地址端口6.7.8.9:8000發(fā)送一個數(shù)據(jù)報后,192.168.0.8才能收到6.7.8.9:8000發(fā)送到1.2.3.4:62000的數(shù)據(jù)報。
2. Symmetric NAT(對稱NAT):
特點:對每個外部主機或端口的會話都會映射為不同的端口(洞)。
表現(xiàn)形式:只有來自同一內部IP:PORT、且針對同一目標IP:PORT的請求才被NAT轉換至同一個公網(外部)IP:PORT,否則的話,NAT將為之分配一個新的外部(公網)IP:PORT。并且,只有曾經收到過內部主機請求的外部主機才能向內部主機發(fā)送數(shù)據(jù)包。
對稱的NAT不保證所有會話中的(私有地址,私有端口)和(公開IP,公開端口)之間綁定的一致性。相反,它為每個新的會話分配一個新的端口號。
NAT的限制:對稱NAT > 端口限制圓錐型 > 限制圓錐型 > 完全圓錐型
不同的NAT組合打洞的方式也有所不同,有點可以打洞,有的則不能打洞,如兩個都是對稱型設備則無法實現(xiàn)打洞。不同組合打洞結果如下:
| 全錐型 | 全錐型 | 是 |
| 全錐型 | 受限錐型 | 是 |
| 全錐型 | 端口受限錐型 | 是 |
| 全錐型 | 對稱型 | 是 |
| 受限錐型 | 受限錐型 | 是 |
| 受限錐型 | 端口受限錐型 | 是 |
| 受限錐型 | 對稱型 | 是 |
| 端口受限錐型 | 端口受限錐型 | 是 |
| 端口受限錐型 | 對稱型 | 否 |
| 對稱型 | 對稱型 | 否 |
4.NAT類型檢測
前提條件:有一個公網的Server并且綁定了兩個公網IP(IP-1,IP-2)。這個Server做UDP監(jiān)聽(IP-1,Port-1),(IP-2,Port-2)并根據(jù)客戶端的要求進行應答。
第一步:檢測客戶端是否有能力進行UDP通信以及客戶端是否位于NAT后?
客戶端建立UDP socket,然后用這個socket向服務器(IP-1,Port-1)發(fā)送數(shù)據(jù)包,要求服務器返回客戶端的IP和Port。客戶端發(fā)送請求后立即開始接受數(shù)據(jù)包,要設定socket Timeout(300ms),防止無限堵塞. 重復這個過程若干次。如果每次都超時,無法接受到服務器的回應,則說明客戶端無法進行UDP通信,可能是防火墻或NAT阻止UDP通信,這樣的客戶端也就不能P2P了(檢測停止)。
當客戶端能夠接收到服務器的回應時,需要把服務器返回的客戶端(IP,Port)和這個客戶端socket的 (LocalIP,LocalPort)比較。如果完全相同則客戶端不在NAT后,這樣的客戶端具有公網IP可以直接監(jiān)聽UDP端口接收數(shù)據(jù)進行通信(檢測停止)。否則客戶端在NAT后要做進一步的NAT類型檢測(繼續(xù))。
第二步:檢測客戶端NAT是否是Full Cone NAT?
客戶端建立UDP socket,然后用這個socket向服務器(IP-1,Port-1)發(fā)送數(shù)據(jù)包,要求服務器用另一對(IP-2,Port-2)響應客戶端的請求往回發(fā)一個數(shù)據(jù)包。客戶端發(fā)送請求后立即開始接受數(shù)據(jù)包,要設定socket Timeout(300ms),防止無限堵塞. 重復這個過程若干次。如果每次都超時,無法接受到服務器的回應,則說明客戶端的NAT不是一個Full Cone NAT,具體類型有待下一步檢測(繼續(xù))。
如果能夠接受到服務器從(IP-2,Port-2)返回的應答UDP包,則說明客戶端是一個Full Cone NAT,這樣的客戶端能夠進行UDP-P2P通信(檢測停止)。
第三步:檢測客戶端NAT是否是Symmetric NAT?
客戶端建立UDP socket,然后用這個socket向服務器(IP-1,Port-1)發(fā)送數(shù)據(jù)包,要求服務器返回客戶端的IP和Port, 客戶端發(fā)送請求后立即開始接受數(shù)據(jù)包,要設定socket Timeout(300ms),防止無限堵塞. 重復這個過程直到收到回應(一定能夠收到,因為第一步保證了這個客戶端可以進行UDP通信)。
用同樣的方法用同一個socket向服務器的(IP-2,Port-2)發(fā)送數(shù)據(jù)包要求服務器返回客戶端的IP和Port。
比較上面兩個過程從服務器返回的客戶端(IP,Port),如果兩個過程返回的(IP,Port)有一個不同(IP不同或者port不同),則說明客戶端為Symmetric NAT,這樣的客戶端無法進行UDP-P2P通信(檢測停止)。
否則是Restricted Cone NAT,是否為Port Restricted Cone NAT有待檢測(繼續(xù))。
第四步:檢測客戶端NAT是否是Restricted Cone NAT還是Port Restricted Cone NAT?
客戶端建立UDP socket,然后用這個socket向服務器(IP-1,Port-1)發(fā)送數(shù)據(jù)包,要求服務器用IP-1和一個不同于Port-1的端口發(fā)送一個UDP數(shù)據(jù)包響應客戶端, 客戶端發(fā)送請求后立即開始接受數(shù)據(jù)包,要設定socket Timeout(300ms),防止無限堵塞. 重復這個過程若干次。如果每次都超時,無法接受到服務器的回應,則說明客戶端是一個Port Restricted Cone NAT,如果能夠收到服務器的響應則說明客戶端是一個Restricted Cone NAT。以上兩種NAT都可以進行UDP-P2P通信。
三、P2P通信
根據(jù)客戶端的不同,客戶端之間進行P2P傳輸?shù)姆椒ㄒ猜杂胁煌?#xff0c;這里介紹了現(xiàn)有的穿越中間件進行P2P通信的幾種技術。
1 中繼(Relaying)
這是最可靠但也是最低效的一種P2P通信實現(xiàn)。其原理是通過一個有公網IP的服務器中間人對兩個內網客戶端的通信數(shù)據(jù)進行中繼和轉發(fā)。如下圖所示:
Server S||+----------------------+----------------------+| |NAT A NAT B| || | Client A Client B客戶端A和客戶端B不直接通信,而是先都與服務端S建立鏈接,然后再通過服務器S和對方建立的通路來中繼傳遞的數(shù)據(jù)。這種方法的缺陷很明顯, 當鏈接的客戶端變多之后,會顯著增加服務器的負擔,完全沒體現(xiàn)出P2P的優(yōu)勢。但這種方法的好處是能保證成功,因此在實踐中也常作為一種備選方案。
2 逆向鏈接(Connection reversal)
第二種方法在當兩個端點中有一個不存在中間件的時候有效。例如,客戶端A在NAT之后,而客戶端B擁有全局IP地址,如下圖:
Server S18.181.0.31:1235||+----------------------+----------------------+| |NAT A | 155.99.25.11:62000 || || |Client A Client B10.0.0.1:1234 138.76.29.7:1234客戶端A內網地址為10.0.0.1,且應用程序正在使用TCP端口1234。A和服務器S建立了一個鏈接,服務器的IP地址為18.181.0.31,監(jiān)聽1235端口。NAT A給客戶端A分配了TCP端口62000,地址為NAT的公網IP地址155.99.25.11, 作為客戶端A對外當前會話的臨時IP和端口。因此S認為客戶端A就是155.99.25.11:62000。而B由于有公網地址,所以對S來說B就是138.76.29.7:1234。
當客戶端B想要發(fā)起一個對客戶端A的P2P鏈接時,要么鏈接A的外網地址155.99.25.11:62000,要么鏈接A的內網地址10.0.0.1:1234,然而兩種方式鏈接都會失敗。
鏈接10.0.0.1:1234失敗自不用說,為什么鏈接155.99.25.11:62000也會失敗呢?來自B的TCP SYN握手請求到達NAT A的時候會被拒絕,因為對NAT A來說只有外出的鏈接才是允許的。
在直接鏈接A失敗之后,B可以通過S向A中繼一個鏈接請求,從而從A方向“逆向“地建立起A-B之間的點對點鏈接。
很多當前的P2P系統(tǒng)都實現(xiàn)了這種技術,但其局限性也是很明顯的,只有當其中一方有公網IP時鏈接才能建立。越來越多的情況下, 通信的雙方都在NAT之后,因此就要用到我們下面介紹的第三種技術了。
3 UDP打洞(UDP hole punching)
第三種P2P通信技術,被廣泛采用的,名為“P2P打洞“。P2P打洞技術依賴于通常防火墻和錐型NAT允許正當?shù)腜2P應用程序在中間件中打洞且與對方建立直接鏈接的特性。 以下主要考慮兩種常見的場景,以及應用程序如何設計去完美地處理這些情況。第一種場景代表了大多數(shù)情況,即兩個需要直接鏈接的客戶端處在兩個不同的NAT 之后;第二種場景是兩個客戶端在同一個NAT之后,但客戶端自己并不需要知道。
a) 端點在不同的NAT之后
假設客戶端A和客戶端B的地址都是內網地址,且在不同的NAT后面。A、B上運行的P2P應用程序和服務器S都使用了UDP端口1234,A和B分別初始化了 與Server的UDP通信,地址映射如圖所示:
Server S18.181.0.31:1234||+----------------------+----------------------+| |NAT A NAT B 155.99.25.11:62000 138.76.29.7:31000| || |Client A Client B10.0.0.1:1234 10.1.1.3:1234現(xiàn)在假設客戶端A打算與客戶端B直接建立一個UDP通信會話。如果A直接給B的公網地址138.76.29.7:31000發(fā)送UDP數(shù)據(jù),NAT B將很可能會無視進入的數(shù)據(jù)(除非是Full Cone NAT),因為源地址和端口與S不匹配,而最初只與S建立過會話。B往A直接發(fā)信息也類似。
假設A開始給B的公網地址發(fā)送UDP數(shù)據(jù)的同時,給服務器S發(fā)送一個中繼請求,要求B開始給A的公網地址發(fā)送UDP信息。A往B的輸出信息會導致NAT A打開 一個A的內網地址與與B的外網地址之間的新通訊會話,B往A亦然。一旦新的UDP會話在兩個方向都打開之后,客戶端A和客戶端B就能直接通訊, 而無須再通過引導服務器S了。
UDP打洞技術有許多有用的性質。一旦一個的P2P鏈接建立,鏈接的雙方都能反過來作為“引導服務器”來幫助其他中間件后的客戶端進行打洞,極大減少了服務器的負載。應用程序不需要知道中間件具體是什么(如果有的話),因為以上的過程在沒有中間件或者有多個中間件的情況下也一樣能建立通信鏈路。
b) 端點在相同的NAT之后
現(xiàn)在考慮這樣一種情景,兩個客戶端A和B正好在同一個NAT之后(而且可能他們自己并不知道),因此在同一個內網網段之內。 客戶端A和服務器S建立了一個UDP會話,NAT為此分配了公網端口62000,B同樣和S建立會話,分配到了端口62001,如下圖:
Server S18.181.0.31:1234||NATA-S 155.99.25.11:62000B-S 155.99.25.11:62001|+----------------------+----------------------+| |Client A Client B 10.0.0.1:1234 10.1.1.3:1234假設A和B使用了上節(jié)介紹的UDP打洞技術來建立P2P通路,那么會發(fā)生什么呢?
首先A和B會得到由S觀測到的對方的公網IP和端口號,然后給對方的地址發(fā)送信息。 兩個客戶端只有在NAT允許內網主機對內網其他主機發(fā)起UDP會話的時候才能正常通信,我們把這種情況稱之為”回環(huán)傳輸“(lookback translation),因為從內部到達NAT的數(shù)據(jù)會被“回送”到內網中而不是轉發(fā)到外網。
例如,當A發(fā)送一個UDP數(shù)據(jù)包給B的公網地址時,數(shù)據(jù)包最初有源IP地址和端口地址10.0.0.1:1234和 目的地址155.99.25.11:62001,NAT收到包后,將其轉換為源155.99.25.11:62000(A的公網地址)和目的10.1.1.3:1234,然后再轉發(fā)給B。即便NAT支持回環(huán)傳輸,這種轉換和轉發(fā)在此情況下也是沒必要的,且有可能會增加A與B的對話延時和加重NAT的負擔。
對于這個情況,優(yōu)化方案是很直觀的。當A和B最初通過S交換地址信息時,他們應該包含自身的IP地址和端口號(從自己看),同時也包含從服務器看的自己的 地址和端口號。然后客戶端同時開始從對方已知的兩個的地址中同時開始互相發(fā)送數(shù)據(jù),并使用第一個成功通信的地址作為對方地址。
如果兩個客戶端在同一個 NAT后,發(fā)送到對方內網地址的數(shù)據(jù)最有可能先到達,從而可以建立一條不經過NAT的通信鏈路;如果兩個客戶端在不同的NAT之后,發(fā)送給對方內網地址的數(shù)據(jù)包根本就到達不了對方,但仍然可以通過公網地址來建立通路。
值得一提的是,雖然這些數(shù)據(jù)包通過某種方式驗證,但是在不同NAT的情況下完全有可能會導致A往B發(fā)送的信息發(fā)送到其他A內網網段中無關的結點上去的。
c) 端點在多級NAT之后
在一些拓樸結構中,可能會存在多級NAT設備,在這種情況下,如果沒有關于拓樸的具體信息, 兩個Peer要建立“最優(yōu)”的P2P鏈接是不可能的,下面來說為什么。以下圖為例:
Server S18.181.0.31:1234||NAT XA-S 155.99.25.11:62000B-S 155.99.25.11:62001||+----------------------+----------------------+| |NAT A NAT B 192.168.1.1:30000 192.168.1.2:31000| || |Client A Client B10.0.0.1:1234 10.1.1.3:1234假設NAT X是一個網絡提供商ISP部署的工業(yè)級NAT,其下子網共用一個公網地址155.99.25.11,NAT A和NAT B分別是其下不同用戶的網關部署的NAT。只有服務器S 和NAT X有全局的路由地址。Client A在NAT A的子網中,同時Client B在NAT B的子網中,每經過一級NAT都要進行一次網絡地址轉換。
現(xiàn)在假設A和B打算建立直接P2P鏈接,用一般的方法(通過Server S來打洞)自然是沒問題的,那能不能優(yōu)化呢?一種想當然的優(yōu)化辦法是A直接把信息發(fā)送給NAT B的內網地址192.168.1.2:31000,且B通過NAT B把信息發(fā)送給A的路由地址192.168.1.1:30000,不幸的是,A和B都沒有辦法得知這兩個目的地址,因為S只看見了客戶端 ‵全局‵地址155.99.25.11。退一步說,即便A和B通過某種方法得知了那些地址,我們也無法保證他們是可用的。因為ISP分配的子網地址可能和NAT A B分配的子網地址 域相沖突。因此客戶端沒有其他選擇,只能使用S來進行打洞并進行回環(huán)傳輸。
打洞流程
假設A現(xiàn)在希望建立一條到B的udp會話,那么這個建立基本流程是:
四.代碼示例(golang)
服務端 server.go
package mainimport ("fmt""net""time" )const serverPort = 9000func main() {// 監(jiān)聽UDPconn, err := net.ListenUDP("udp4", &net.UDPAddr{IP: net.IPv4zero,Port: serverPort,})if err != nil {fmt.Printf("監(jiān)聽失敗:%s\n", err)return}fmt.Printf("開始監(jiān)聽:[%s]\n", conn.LocalAddr().String())// 釋放資源defer conn.Close()// 存放連接的客戶端二元組peers := make([]*net.UDPAddr, 0, 2)b := make([]byte, 500)for {n, addr, err := conn.ReadFromUDP(b)if err != nil {fmt.Printf("讀取信息失敗:%s\n", err)return}// 將鏈接存起來peers = append(peers, addr)// 接受到的消息fmt.Printf("收到客戶端[%s]的消息:%s\n", addr.String(), b[:n])// 如果有2條鏈接了,就給客戶端響應另一個客戶端二元組if len(peers) == 2 {fmt.Printf("可以進行UDP打洞,建立[%s]--[%s]的連接\n", peers[0].String(), peers[1].String())conn.WriteToUDP([]byte(peers[1].String()), peers[0])conn.WriteToUDP([]byte(peers[0].String()), peers[1])time.Sleep(3 * time.Second)fmt.Println("中轉服務器退出,仍不影響peers通信")return}} }客戶端 client.go
package mainimport ("fmt""net" )// 服務器地址(注:改為真實使用的服務器地址) const serverAddr = "127.0.0.1:9000" // 本地客戶端端口 const srcPort = 8888func main() {// 第一步:與服務建立UDP連接,接收服務器發(fā)送的另一個客戶端的地址信息srcAddr := &net.UDPAddr{IP: net.IPv4zero, Port: srcPort}raddr, _ := net.ResolveUDPAddr("udp4", serverAddr)fmt.Printf("本機地址[%s]\n", srcAddr)conn, err := net.DialUDP("udp4", srcAddr, raddr)if err != nil {fmt.Printf("連接服務器失敗:%s\n", err)return}_, err = conn.Write([]byte("hi"))if err != nil {fmt.Printf("與服務器發(fā)送消息失敗:%s\n", err)return}fmt.Println("與服務器發(fā)送消息成功,等待響應...")// 開始等待服務器響應消息b := make([]byte, 500)n, _, err := conn.ReadFromUDP(b)if err != nil {fmt.Printf("接收數(shù)據(jù)失敗:%s\n", err)return}// 與服務器的連接要斷開!!conn.Close()// 第二步:與另一個客戶端建立UDP連接,以此“打洞”// 另一個客戶端dstAddr, _ := net.ResolveUDPAddr("udp4", string(b[:n]))udpConn, err := net.DialUDP("udp4", srcAddr, dstAddr)if err != nil {fmt.Printf("與客戶端[%s]創(chuàng)建UDP失敗:%s\n", dstAddr, err)return}defer udpConn.Close()// 向另一個客戶端發(fā)送一條udp消息(對方的nat設備會丟棄該消息,非法來源),// 用意是在自身的nat設備打開一條可進入的通道,這樣對方就可以發(fā)過來udp消息_, err = udpConn.Write([]byte("你好"))if err != nil {fmt.Printf("與客戶端發(fā)送消息失敗:%s\n", err)return}fmt.Printf("與客戶端[%s]建立成功,可以通信\n", dstAddr)// 標準輸入內容go func() {msg := ""for {fmt.Scanln(&msg)_, err = udpConn.Write([]byte(msg))if err != nil {fmt.Printf("發(fā)送信息失敗:%s\n", err)continue}}}()// 接收另一個客戶端信息data := make([]byte, 500)for {dnum, _, err := udpConn.ReadFromUDP(data)if err != nil {fmt.Printf("接收信息失敗:%s\n", err)continue}fmt.Printf("來自[%s]的信息 >> %s\n", dstAddr, data[:dnum])} }總結
以上是生活随笔為你收集整理的内网穿透实现P2P通信的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 占空比输出程序c语言,关于51单片机输出
- 下一篇: 品优购商城——列表页