多是由於以前折騰過搭網站之類的事情的緣由,我我的對計算機網絡比較感興趣。半年以前試過花生殼的內網穿透服務以後寫過一篇文章談內網穿透的原理,可是最近發給同窗看時候他說打不開,才知道被博客園和諧了,趁機回顧再看才發現以前寫的內容也有問題(那時候尚未學計算機網絡),因此今天又參考了許多資料,力圖正確、準確地解讀一下NAT和內網穿透,這兩個隨着ipv6的普及極可能會被淘汰、可是我我的以爲短期不可能徹底淘汰的技術。html
用我本身的狀況作個例子吧,個人網絡狀況是:java
我來詳細描述一下個人設備和一臺公網ip下的服務器通訊的過程,也算是複習計算機網絡了吧,這裏假設個人電腦已經知道目標設備的ip地址了(也就是不算DNS部分了):api
設備發送數據包,源ip是房間路由器建立的內網下的ip,端口號是socket隨機選擇的,其餘的目標ip、源mac都是肯定的,目標mac填的是本身路由器的mac地址服務器
設備會用本機配置的24位子網掩碼與目標地址進行「與」運算,得出目標地址與本機不是同一網段,所以發送目標的數據包須要通過路由器的轉發。 -[4]網絡
路由器先把源ip地址改爲本身在樓中使用的那個內網ip,爲設備分配一個惟一的端口號,並在路由表中記錄下這一映射關係。根據路由表中的記錄(肯定要訪問的地址不是樓裏的地址,是公網的地址)獲取下一跳的ip地址, 並根據ARP協議肯定下一跳(也就是每層的交換機)的MAC地址 ,將目標mac地址修改成下一跳的mac地址,而後在數據鏈路層和網絡層中進行傳輸session
傳到每一層的交換機時,不須要修改源ip地址(由於是交換機,沒有修改Ip的功能),只把目標mac改成NAT路由器的地址,而後再經過數據鏈路層和網絡層傳輸socket
到了NAT服務器,服務器隨機爲其分配一個端口[3],並在Track Table中保存這個[內網ip:端口號->目標ip:端口號]的映射,這一過程稱爲鏈接跟蹤。注意此時的內網ip指的是路由器的ip,端口號是路由器爲設備分配的端口號tcp
服務器收到數據包,再向NAT路由器返回數據包,NAT路由器經過查詢Track table肯定內網ip(是路由器的內網ip)和端口號,並改變目標Ip爲此,返回發送給每層樓的交換機,交換機經過arp協議藉助路由器的ip肯定路由器的mac地址,並將目標mac地址改成此,再發送給路由器網站
路由器經過端口號肯定是哪一臺內部設備,轉發給該設備.net
這裏對於NAT,關鍵的一步是4,NAT只會接受在Track table中有ip和端口記錄的外部訪問,其餘的都一律不轉發,這也就是咱們常說的NAT只能內網訪問外網,不能外網訪問內網
可是不得不說,P2P的需求是真實存在的,爲了解決NAT帶來的問題,內網穿透誕生了。
這篇文章裏比較好地提到了內網穿透的原理,現摘錄出來:
假設如今有內網客戶端A和內網客戶端B,有公網服務端S。
若是A和B想要進行UDP通訊,則必須穿透雙方的NAT路由。假設爲NAT-A和NAT-B。A發送數據包到公網S,B發送數據包到公網S,則S分別獲得了A和B的公網IP,
S也和A B 分別創建了會話,由S發到NAT-A的數據包會被NAT-A直接轉發給A,
由S發到NAT-B的數據包會被NAT-B直接轉發給B,除了S發出的數據包以外的則會被丟棄。
因此:如今A B 都能分別和S進行全雙工通信了,可是A B之間還不能直接通信。解決辦法是:A向B的公網IP發送一個數據包,則NAT-A能接收來自NAT-B的數據包
並轉發給A了(即B如今能訪問A了);再由S命令B向A的公網IP發送一個數據包,則
NAT-B能接收來自NAT-A的數據包並轉發給B了(即A如今能訪問B了)。以上就是「打洞」的原理。
爲了保證A的路由器有與B的session,A要定時與B作心跳包,一樣,B也要定時與A作心跳,這樣,雙方的通訊通道都是通的,就能夠進行任意的通訊了。
圖解以下:
上面說的就是UDP打洞的原理,可是爲何是UDP呢?
UDP的socket容許多個socket綁定到同一個本地端口,而TCP的socket則不容許。
這是這樣一個意思:A B要鏈接到S,確定首先A B雙方都會在本地建立一個socket,
去鏈接S上的socket。建立一個socket必然會綁定一個本地端口(就算應用程序裏面沒寫
端口,實際上也是綁定了的,至少java確實如此),假設爲8888,這樣A和B才分別創建了到
S的通訊信道。接下來就須要打洞了,打洞則須要A和B分別發送數據包到對方的公網IP。可是
問題就在這裏:由於NAT設備是根據端口號來肯定session,若是是UDP的socket,A B能夠
分別再建立socket,而後將socket綁定到8888,這樣打洞就成功了。可是若是是TCP的
socket,則不能再建立socket並綁定到8888了,這樣打洞就沒法成功。
道理的確是這麼個道理,可是博主說的還不夠清楚,我再解讀下,就用上面原博主給的例子了:
因爲NAT的外部端口是隨機指定的,若是A和B分別和服務器通訊,都使用8888端口的話,若是A要和B直接打洞且不用8888端口,就會遇到一個問題:A不知道B未來包發出來NAT-B會給它分配什麼接口,因此A就沒辦法指定目標Ip的端口號(由於NAT-B是隨機分配端口的,B即便知道了A用了哪一個端口打洞也沒辦法讓NAT-B去使用這個特定的端口。綜上,A和B能使用的,只有和服務器鏈接時已經建立在track table中的那個端口,也就是咱們例子中的8888了。
注意track table中的記錄是有有效期的,因爲咱們不知道外部設備何時會訪問咱們在內網中的設備,因此咱們須要保證設備和服務器之間的鏈接不能斷開,track table中的映射不能被銷燬,因此須要在必定的時間間隔以後發包來維持NAT中的映射關係,這就是爲何咱們用花生殼的時候它一直要求咱們「保持在線」的緣由了
可是TCP也不是不能進行穿透,這就須要用到端口重用了:
tcp打洞也須要NAT設備支持才行。
tcp的打洞流程和udp的基本同樣,但tcp的api決定了tcp打洞的實現過程和udp不同。
tcp按cs方式工做,一個端口只能用來connect或listen,因此須要使用端口重用,才能利用本地nat的端口映射關係。(設置SO_REUSEADDR,在支持SO_REUSEPORT的系統上,要設置這兩個參數。)鏈接過程:(以udp打洞的第2種狀況爲例(典型狀況))
nat後的兩個peer,A和B,A和B都bind本身listen的端口,向對方發起鏈接(connect),即便用相同的端口同時鏈接和等待鏈接。由於A和B發出鏈接的順序有時間差,假設A的syn包到達B的nat時,B的syn包尚未發出,那麼B的nat映射尚未創建,會致使A的鏈接請求失敗(鏈接失敗或沒法鏈接,若是nat返回RST或者icmp差錯,api上可能表現爲被RST;有些nat不返回信息直接丟棄syn包(反而更好)),(應用程序發現失敗時,不能關閉socket,closesocket()可能會致使NAT刪除端口映射;隔一段時間(1-2s)後未鏈接還要繼續嘗試);但後發B的syn包在到達A的nat時,因爲A的nat已經創建的映射關係,B的syn包會經過A的nat,被nat轉給A的listen端口,從而進去三次握手,完成tcp鏈接。
另外,NAT還有許多其餘功能,更詳細的介紹能夠看這篇文章