網上找了不少,代碼大堆,原理講清楚透徹的很少。html
本人找幾篇講得好的來整理一下。nginx
一片技術文章,最主要的講清楚原理,若是再有完整的能運行的源代碼也可,關鍵是要把核心部分代碼分析清楚。數據庫
(1)問題的由來:服務器
大部分的電腦上網都是用動態ip地址。內網的ip是由net(路由、網關)分配的,net發出去的時候,映射到一個公網地址,這是一個動態計算的過程(特別是端口號),所以稱爲動態ip地址。外部網是沒法直接訪問內網計算機的,但在大部分狀況下,藉助一臺有公網ip地址電腦(這裏叫服務器),經過某種方式,能夠實現鏈接,這種技術較「打洞」。網絡
(2)動態ip具體分析:session
如圖:併發
有一個私有網絡192.168.0.2,client a是其中的一臺計算機,這個網絡的網關natA(一個nat設備)的外網ip是202.103.142.29(應該還有一個內網的ip地址,好比10.0.0.10)。若是client a中的某個進程(這個進程建立了一個socket,這個socket綁定1234端口)想訪問外網主機129.208.12.38的2000端口,那麼當數據包經過nat時會發生什麼事情呢?socket
先nat會改變這個數據包的原ip地址,改成202.103.142.29。接着nat會爲這個傳輸建立一個session(session是一個抽象的概念,若是是tcp,也許session是由一個syn包開始,以一個fin包結束。而udp呢,以這個ip的這個端口的第一個udp開始,結束不肯定,也許是幾分鐘,也許是幾小時,這要看具體的實現了)而且給這個session分配一個端口,好比62000,而後改變這個數據包的源端口爲62000。因此原本是tcp
(192.168.0.2:1234->129.208.12.38:2000)post
的數據包到了互聯網上變爲了
(202.103.142.29:62000->129.208.12.38:2000)。
一旦nat建立了一個session後,nat會記住62000端口對應的是192.168.0.2的1234端口,之後從129.208.12.38發送到62000端口的數據會被nat自動的轉發到192.168.0.2上。(注意:這裏是說129.208.12.38發送到62000端口的數據會被轉發,其餘的ip發送到這個端口的數據將被nat拋棄,這就是真正頭痛的問題)這樣client a就與server s1創建以了一個鏈接。
若是client a的原來那個socket(綁定了1234端口的那個udp socket)又接着向另一個服務器server s2發送了一個udp包,那麼這個udp包在經過nat時會怎麼樣呢?
答案是net決定的,不一樣的net有不一樣的答案。
這裏須要介紹一下NAT的類型:
NAT設備的類型對於TCP穿越NAT,有着十分重要的影響,根據端口映射方式,NAT可分爲以下4類,前3種NAT類型可統稱爲cone類型。
(1)全克隆( Full Cone) : NAT把全部來自相同內部IP地址和端口的請求映射到相同的外部IP地址和端口。任何一個外部主機都可經過該映射發送IP包到該內部主機。
(2)限制性克隆(Restricted Cone) : NAT把全部來自相同內部IP地址和端口的請求映射到相同的外部IP地址和端口。可是,只有當內部主機先給IP地址爲X的外部主機發送IP包,該外部主機才能向該內部主機發送IP包。
(3)端口限制性克隆( Port Restricted Cone) :端口限制性克隆與限制性克隆相似,只是多了端口號的限制,即只有內部主機先向IP地址爲X,端口號爲P的外部主機發送1個IP包,該外部主機纔可以把源端口號爲P的IP包發送給該內部主機。
(4)對稱式NAT ( Symmetric NAT) :這種類型的NAT與上述3種類型的不一樣,在於當同一內部主機使用相同的端口與不一樣地址的外部主機進行通訊時, NAT對該內部主機的映射會有所不一樣。對稱式NAT不保證全部會話中的私有地址和公開IP之間綁定的一致性。相反,它爲每一個新的會話分配一個新的端口號。
第一種最爲理想,基本就是無需打洞;
第四種最糟糕,根本就不能打洞。好消息就是這種net基本沒有,不多。
因此關鍵是第二和第三種net類型。
所以打洞的本質就是利用net的特性「只有當內部主機先給IP地址爲X的外部主機發送IP包,該外部主機才能向該內部主機發送IP包」
(3)實現步驟(各個實現可能都不同)
咱們先假設一下:有一個服務器S在公網上有一個IP,兩個私網分別由NAT-A和NAT-B鏈接到公網,NAT-A後面有一臺客戶端A,NAT-B 後面有一臺客戶端B,如今,咱們須要藉助S將A和B創建直接的TCP鏈接,即由B向A打一個洞,讓A能夠沿這個洞直接鏈接到B主機,就好像NAT-B不存在同樣。
實現過程以下:
一、 S啓動兩個網絡偵聽,一個叫【主鏈接】偵聽,一個叫【協助打洞】的偵聽。
二、 A和B分別與S的【主鏈接】保持聯繫。
三、 當A須要和B創建直接的TCP鏈接時,首先鏈接S的【協助打洞】端口,併發送協助鏈接申請。同時在該端口號上啓動偵聽(保證net類型3也能成功)。注意因爲要在相同的網絡終端上綁定到不一樣的套接字上,因此必須爲這些套接字設置 SO_REUSEADDR 屬性(即容許重用),不然偵聽會失敗。
四、 S的【協助打洞】鏈接收到A的申請後經過【主鏈接】通知B,並將A通過NAT-A轉換後的公網IP地址和端口等信息告訴B。
五、 B收到S的鏈接通知後首先與S的【協助打洞】端口鏈接,隨便發送一些數據後當即斷開,這樣作的目的是讓S能知道B通過NAT-B轉換後的公網IP和端口號。
六、 B嘗試與A的通過NAT-A轉換後的公網IP地址和端口進行connect(這就是所謂「打洞」),根據不一樣的路由器會有不一樣的結果,有些路由器在這個操做就能創建鏈接(例如我用的TPLink R402),大多數路由器對於不請自到的SYN請求包直接丟棄而致使connect失敗,但NAT-A會紀錄這次鏈接的源地址和端口號,爲接下來真正的鏈接作好了準備,這就是所謂的打洞,即B向A打了一個洞,下次A就能直接鏈接到B剛纔使用的端口號了。
七、 客戶端B打洞的同時在相同的端口上啓動偵聽。B在一切準備就緒之後經過與S的【主鏈接】回覆消息「我已經準備好」,S在收到之後將B通過NAT-B轉換後的公網IP和端口號告訴給A。
八、 A收到S回覆的B的公網IP和端口號等信息之後,開始鏈接到B公網IP和端口號,因爲在步驟6中B曾經嘗試鏈接過A的公網IP地址和端口,NAT-A紀錄 了這次鏈接的信息,因此當A主動鏈接B時,NAT-B會認爲是合法的SYN數據,並容許經過,從而直接的TCP鏈接創建起來了。
(4)讓內網主機作服務器
以上的應用主要在於作p2p軟件,若是咱們想用家裏的電腦,作服務器,是不須要這項技術(「打洞」)的,應爲咱們能夠把路由器設置爲net1型,能夠直接設置端口映射:
但仍是須要一個外網固定ip的服務器來負責通知其餘客服端,由於撥號時獲得的ip(這其實也是外網ip,就是其餘的計算機能夠直接訪問你的)每次仍是變化的。
(5)花生殼是什麼
花生殼是能夠把這個變化的ip映射爲固定域名的域名服務商,若是不須要域名服務,只要用ip訪問便可的應用(好比數據庫),是用不上這個軟件的。
可是還有一種辦法,若是你已經有一臺有靜態ip的服務器,在上面部署一個代理服務器(好比nginx),而後把這個動態的ip通知它就能夠了。
出處:http://www.cnblogs.com/eyye/archive/2012/10/23/2734807.html