其實很早我就已經實現了使用TCP協議穿透NAT了,但是苦於一直沒有時間,所以沒有寫出來,現在終於放假有一點空閒,於是寫出來共享之。
一直以來,說起NAT穿透,很多人都會被告知使用UDP打孔這個技術,基本上沒有人會告訴你如何使用TCP協議去 穿透(甚至有的人會直接告訴你TCP協議是無法實現穿透的)。但是,眾所周知的是,UDP是一個無連接的數據報協議,使用它就必須自己維護收發數據包的完 整性,這常常會大大增加程序的復雜度,而且一些程序由於某些原因,必須使用TCP協議,這樣就常常令一些開發TCP網絡程序的人員“談穿透色變”。那麼, 使用TCP協議是不是就不能實現穿透呢?答案當然是否定的:TCP協議不僅能實現NAT穿透,而且實現起來比UDP穿透甚至還簡單一些。
要了解如何使用TCP穿透NAT,就要首先看看如何使用UDP穿透NAT.我們假設在兩個不同的局域網後面分別有2台客戶機A和 B,AB所在的局域網都分別通過一個路由器接入互聯網。互聯網上有一台服務器S.現在AB是無法直接和對方發送信息的,AB都不知道對方在互聯網上真正的IP和端口, AB所在的局域網的路由器只允許內部向外主動發送的信息通過。對於B直接發送給A的路由器的消息,路由會認為其“不被信任”而直接丟棄。
要實現 AB直接的通訊,就必須進行以下3步:A首先連接互聯網上的服務器S 並發送一條消息(對於UDP這種無連接的協議其實直接初始會話發送消息即可),這樣S就獲取了A在互聯網上的實際終端(發送消息的IP和端口號)。接著 B也進行同樣的步驟,S就知道了AB在互聯網上的終端(這就是“打洞”)。接著S分別告訴A和B對方客戶端在互聯網上的實際終端,也即S告訴A客戶B的會 話終端,S告訴B客戶A的會話終端。這樣,在AB都知道了對方的實際終端之後,就可以直接通過實際終端發送消息了(因為先前雙方都向外發送過消息,路由上 已經有允許數據進出的消息通道)。
用UDP來實現以上3步不存在什麼理論上的問題,因為UDP是無連接的協議,它允許socket進行“多對一”的通訊(即幾個具有不同IP和端口號的 socket向一個接收socket發送消息)。但是使用TCP就出現了問題:在一般情況下,TCP socket不允許在已經建立連接的端口上再進行監聽和使用該本地端口。換句話說,當AB連接上服務器S後,S將AB的實際終端告訴對方,下一步本該是 AB利用對方的實際終端進行直連,但這時你會發現對方的實際終端已經被占用了(就是各自連接到服務器S的會話占用了終端),無法同時listen和 connect.於是很多人得出結論:TCP無法實現NAT穿透。
於是問題的關鍵變成了如何復用一個TCP連接的本地終端,這其實不是協議的問題,而是一個API的問題。幸運的是,所有主流操作系統都支持一個特定的 TCP套接字選項——SO_REUSEADDR.這個選項允許將多個socket綁定到同一個本地終端。我們建立socket的時候只要加上這麼一行:
setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, &flag, len) ; //C++就這麼做
_Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, True) '這是vb.net 更加簡單
知道上面的知識就很好辦了,下面我來說說TCP協議的穿透流程:機器布局還是和上面使用UDP的一樣。現在假設客戶A想和客戶B建立TCP連接。
首先還是 AB分別和服務器S分別建立連接,S記錄AB的互聯網實際終端。然後S分別向AB發送對方的實際終端。接著,從A和B向S連接時使用的端口,AB都異步調 用connect函數連接對方的實際終端(就是S告訴的終端),同時,AB雙方都在同一個本地端口監聽到來的連接(也可以先監聽,再connect更 好)。由於雙方都向對方發送了connect請求(假設各自的SYN封包已經穿過了自己的NAT),因此在對方connect請求到達本地的監聽端口時, 路由器會認為這個請求是剛剛那個connect會話的一部分,是已經被許可的,本地監聽端口就會用SYN-ACK響應,同意連接。這樣,TCP穿透NAT 的點對點連接就成功了。