TCP點對點穿透探索
點對點穿透是穿透什么
點對點穿透,需要實現的是對NAT的穿透。想實現NAT的穿透,當然要先了解NAT到底是什么,以及NAT是用來干什么的。
NAT全稱Network Address Translation
,意思是網絡地址轉換
,在1994年提出。它可以對不同的IP及端口進行映射,將一個網絡地址轉換為另一個。NAT的主要用途,大家可以看路由器。路由器具有一個WAN口及多個LAN口;WAN口對外,連接因特網,擁有公網IP;LAN口對內,構建本地網絡,分配的是私網IP。當處於LAN網下的本地主機想要訪問因特網的時候,路由器就會通過NAT技術,將LAN 口的私網IP映射到WAN口的公網IP,實現網絡地址的轉換,這樣本地主機就可以訪問因特網了。
NAT技術的出現,有效減緩了IPv4時代可用IP地址枯竭的問題。至於為何可以緩解IP地址枯竭,我們依舊可以參照路由器來加以理解。路由器的LAN網下擴展了多台本地主機,這些本地主機需要不同的IP地址加以區分。如果它們都是接在英特網下,那么每台主機都需要消耗一個公網IP,但是通過路由器的NAT服務,這些本地主機可以先分配不同的私網IP,然后在需要連接到英特網的時候映射到公網IP的不同端口上完成對因特網的訪問,從而節省IP地址的消耗。至於私網IP地址,由於NAT的存在,本地網絡與英特網處於隔離狀態,不必擔心與其他網絡產生沖突。
上面是對NAT的一些簡單的介紹,以及NAT工作方式的簡單描述,如果覺得難以理解,可以自行查閱更多資料。
舉個例子,假如路由器WAN口獲取到的公網IP是
55.66.77.88
(隨便寫的),LAN網下某台本地主機獲取到的私網IP是192.168.0.100
(路由器多是192.168.0.0
網段)。現在本地主機想要向英特網發起連接,它通過自己的10000
端口發起了連接,路由器知道有本地主機向英特網發起連接,便會分配一個WAN口的可用端口做映射,比如分配到的是5000
端口。這時,192.168.0.100
的10000
端口便和55.66.77.88
的5000
端口產生了映射關系。55.66.77.88
的5000
端口收到的網絡包便會轉發到192.168.0.100
的10000
端口,192.168.0.100
的10000
端口收到的網絡包也會轉發到55.66.77.88
的5000
端口。當然,由於是網絡地址轉換,這途中還會有拆包,重新裝包的過程,不做詳細說明。
大致知道NAT怎么工作的之后,接下來了解為什么要穿透NAT。
按照上面的描述,NAT的工作需要LAN網下的本地設備主動發起網絡連接,然后NAT服務才會將這個連接映射到WAN口的公網IP完成轉換。也就是說,如果本地主機沒有主動發起連接,那么這個映射就不會存在,那么公網上的機器就無法訪問到私網上的機器。也就是說,只能是私網機器主動連接公網機器,而不能是公網機器主動連接私網機器。而為了實現公網機器主動連接私網機器,我們就需要穿透NAT,這就是NAT穿透的由來。
目前比較好實現NAT穿透的方式是采用UDP連接對NAT進行打洞
,然后完成連接。何為打洞
呢?就是為了使NAT產生一個可用的映射。具體步驟就是在私網機器上用UDP向某台公網機器發起連接,使得NAT產生一個可以使用的映射(洞)
。然后通過這個映射(洞)
,就可以穿透NAT。
至於為什么要用UDP,這是由於UDP的某些特性。UDP通信需要先綁定本地機器的端口,完成后就可以從這個端口收發數據,至於從哪里收,發到哪里,可以在收發數據的時候再決定,這也就意味着我可以用這一個端口同時和多個對象通信,只要我收發數據的時候指定不同的對象即可。當本地機器用UDP向英特網上的某個服務器發送數據的時候,這個映射不但能用來和這個服務器進行數據交互,也能用來接收其他主機發來的數據。NAT穿透就是本地主機向公網上的某台服務器發送數據,這時服務器就可以獲得NAT對這台主機的映射,在之前舉得例子中就是55.66.77.88:5000
這個地址。由於NAT會將55.66.77.88:5000
收到的數據轉發至本地主機,所以公網上的其他機器可以從服務器獲取到55.66.77.88:5000
這個網絡地址,然后通過這個網絡地址向私網下的機器發出數據。
而至於為什么不用TCP,也是由於TCP的某些特性。TCP通信的步驟與UDP不同,它需要先在兩個對象之間建立一個專用通道,再用這個通道收發數據。也就是說外人無法插手。這樣一來,雖然其他機器可以通過服務器獲取到NAT的映射對象,也沒辦法利用它向私網下的機器發出數據。
關於TCP與UDP的更多細節,請參考SOCKET編程。
TCP實現點對點穿透的探索
為了嘗試使用TCP實現點對點穿透,需要現對TCP做更多的了解。我之前有詳細查過TCP連接中的各種狀態變化,做了簡單的整理,可以做個參考:TCP連接狀態變化
既然TCP在連接過程中其他人不能插手,但是等它連接結束之后呢?NAT對TCP連接的端口映射在連接結束后就立馬銷毀了嗎?接着深入,發現NAT存在一個老化機制。接下來看看老化是什么意思。NAT生成某個映射后,會將這個映射保存下來,但是即使端口號非常多,它也不是無限的,而既然端口號是有限資源,那么就不能保證映射表的無限擴充。為了合理利用資源,當某個映射一段時間內沒有發生數據交互,NAT就會認為這個映射已經沒有人使用了,就會將這個映射銷毀,回收端口號。這個時間,就叫做老化時間。也就是說,老化是一種映射的回收機制。
從TCP連接狀態變化
中可以知道,TCP在斷開連接后會有一段時間的保護期,不讓這個端口進行下一次連接,這個時間是2*MSL,MSL在協議中的建議值為2分鍾,實際應用中常用是30秒,1分鍾和2分鍾,也就是說這個時間很有可能是一分鍾甚至更久。那老化時間有多久有多久呢,在老化時間控制 中有提到,TCP的默認老化時間是86400秒,TCP-SYN和TCP-FIN的默認老化時間是60秒。這樣說來,按照一般情況等保護期結束的時候,NAT的映射也到期了。
但是沒有關系,SOCKET編程中允許有一些特殊的選項,其中有一個叫SO_REUSEADDR的選項。
以下文字引用自setsockopt中參數之SO_REUSEADDR的意義
setsockopt中參數之SO_REUSEADDR的意義
1、一般來說,一個端口釋放后會等待兩分鍾之后才能再被使用,SO_REUSEADDR是讓端口釋放后立即就可以被再次使用。
SO_REUSEADDR用於對TCP套接字處於TIME_WAIT狀態下的socket,才可以重復綁定使用。server程序總是應該在調用bind()之前設置SO_REUSEADDR套接字選項。TCP,先調用close()的一方會進入TIME_WAIT狀態
2、SO_REUSEADDR和SO_REUSEPORT
SO_REUSEADDR提供如下四個功能:
SO_REUSEADDR允許啟動一個監聽服務器並捆綁其眾所周知端口,即使以前建立的將此端口用做他們的本地端口的連接仍存在。這通常是重啟監聽服務器時出現,若不設置此選項,則bind時將出錯。
SO_REUSEADDR允許在同一端口上啟動同一服務器的多個實例,只要每個實例捆綁一個不同的本地IP地址即可。對於TCP,我們根本不可能啟動捆綁相同IP地址和相同端口號的多個服務器。
SO_REUSEADDR允許單個進程捆綁同一端口到多個套接口上,只要每個捆綁指定不同的本地IP地址即可。這一般不用於TCP服務器。
SO_REUSEADDR允許完全重復的捆綁:當一個IP地址和端口綁定到某個套接口上時,還允許此IP地址和端口捆綁到另一個套接口上。一般來說,這個特性僅在支持多播的系統上才有,而且只對UDP套接口而言(TCP不支持多播)。
SO_REUSEPORT選項有如下語義:
此選項允許完全重復捆綁,但僅在想捆綁相同IP地址和端口的套接口都指定了此套接口選項才行。
如果被捆綁的IP地址是一個多播地址,則SO_REUSEADDR和SO_REUSEPORT等效。
使用這兩個套接口選項的建議:
在所有TCP服務器中,在調用bind之前設置SO_REUSEADDR套接口選項;
當編寫一個同一時刻在同一主機上可運行多次的多播應用程序時,設置SO_REUSEADDR選項,並將本組的多播地址作為本地IP地址捆綁。
if (setsockopt(fd, SOL_SOCKET, SO_REUSEADDR,
(const void *)&nOptval , sizeof(int)) < 0) ...
關於SO_REUSEADDR的特性,網上介紹很多,這里貼出幾個鏈接:
SO_REUSEADDR例解
SO_REUSEADDR 套接字選項應用實例
有了SO_REUSEADDR
就好辦了,在剛斷開連接的時候NAT的映射還沒有被老化,而由於SO_REUSEADDR
套接字選項的關系,也可以立馬進行下一次連接。也就是說只要我們在NAT服務設置的老化時間內重新建立好連接,那么這個映射就可以繼續使用。
從原理上來說應該是存在可行性的,如果有偏頗,忘指正。后續會嘗試搭建環境,寫程序做個試驗。
在這里寫一下之前做的試驗:連接關閉之后NAT的端口映射直接失效,根本無法建立下一次連接,知識儲備還差點兒,太想當然了。