其實(shí)很早我就已經(jīng)實(shí)現(xiàn)了使用TCP協(xié)議穿透NAT了,但是苦于一直沒(méi)有時(shí)間,所以沒(méi)有寫出來(lái),現(xiàn)在終于放假有一點(diǎn)空閑,于是寫出來(lái)共享之。
一直以來(lái),說(shuō)起NAT穿透,很多人都會(huì)被告知使用UDP打孔這個(gè)技術(shù),基本上沒(méi)有人會(huì)告訴你如何使用TCP協(xié)議去穿透(甚至有的人會(huì)直接告訴你TCP協(xié)議是無(wú)法實(shí)現(xiàn)穿透的)。但是,眾所周知的是,UDP是一個(gè)無(wú)連接的數(shù)據(jù)報(bào)協(xié)議,使用它就必須自己維護(hù)收發(fā)數(shù)據(jù)包的完整性,這常常會(huì)大大增加程序的復(fù)雜度,而且一些程序由于某些原因,必須使用TCP協(xié)議,這樣就常常令一些開發(fā)TCP網(wǎng)絡(luò)程序的人員“談穿透色變”。那么,使用TCP協(xié)議是不是就不能實(shí)現(xiàn)穿透呢?答案當(dāng)然是否定的:TCP協(xié)議不僅能實(shí)現(xiàn)NAT穿透,而且實(shí)現(xiàn)起來(lái)比UDP穿透甚至還簡(jiǎn)單一些。
要了解如何使用TCP穿透NAT,就要首先看看如何使用UDP穿透NAT。
我們假設(shè)在兩個(gè)不同的局域網(wǎng)后面分別有2臺(tái)客戶機(jī)A和 B,AB所在的局域網(wǎng)都分別通過(guò)一個(gè)路由器接入互聯(lián)網(wǎng)?;ヂ?lián)網(wǎng)上有一臺(tái)服務(wù)器S。
現(xiàn)在AB是無(wú)法直接和對(duì)方發(fā)送信息的,AB都不知道對(duì)方在互聯(lián)網(wǎng)上真正的IP和端口, AB所在的局域網(wǎng)的路由器只允許內(nèi)部向外主動(dòng)發(fā)送的信息通過(guò)。對(duì)于B直接發(fā)送給A的路由器的消息,路由會(huì)認(rèn)為其“不被信任”而直接丟棄。
要實(shí)現(xiàn) AB直接的通訊,就必須進(jìn)行以下3步:A首先連接互聯(lián)網(wǎng)上的服務(wù)器S并發(fā)送一條消息(對(duì)于UDP這種無(wú)連接的協(xié)議其實(shí)直接初始會(huì)話發(fā)送消息即可),這樣S就獲取了A在互聯(lián)網(wǎng)上的實(shí)際終端(發(fā)送消息的IP和端口號(hào))。接著 B也進(jìn)行同樣的步驟,S就知道了AB在互聯(lián)網(wǎng)上的終端(這就是“打洞”)。接著S分別告訴A和B對(duì)方客戶端在互聯(lián)網(wǎng)上的實(shí)際終端,也即S告訴A客戶B的會(huì)話終端,S告訴B客戶A的會(huì)話終端。這樣,在AB都知道了對(duì)方的實(shí)際終端之后,就可以直接通過(guò)實(shí)際終端發(fā)送消息了(因?yàn)橄惹半p方都向外發(fā)送過(guò)消息,路由上已經(jīng)有允許數(shù)據(jù)進(jìn)出的消息通道)。
用UDP來(lái)實(shí)現(xiàn)以上3步不存在什么理論上的問(wèn)題,因?yàn)閁DP是無(wú)連接的協(xié)議,它允許socket進(jìn)行“多對(duì)一”的通訊(即幾個(gè)具有不同IP和端口號(hào)的socket向一個(gè)接收socket發(fā)送消息)。但是使用TCP就出現(xiàn)了問(wèn)題:在一般情況下,TCP socket不允許在已經(jīng)建立連接的端口上再進(jìn)行監(jiān)聽和使用該本地端口。換句話說(shuō),當(dāng)AB連接上服務(wù)器S后,S將AB的實(shí)際終端告訴對(duì)方,下一步本該是AB利用對(duì)方的實(shí)際終端進(jìn)行直連,但這時(shí)你會(huì)發(fā)現(xiàn)對(duì)方的實(shí)際終端已經(jīng)被占用了(就是各自連接到服務(wù)器S的會(huì)話占用了終端),無(wú)法同時(shí)listen和 connect。于是很多人得出結(jié)論:TCP無(wú)法實(shí)現(xiàn)NAT穿透。
于是問(wèn)題的關(guān)鍵變成了如何復(fù)用一個(gè)TCP連接的本地終端,這其實(shí)不是協(xié)議的問(wèn)題,而是一個(gè)API的問(wèn)題。幸運(yùn)的是,所有主流操作系統(tǒng)都支持一個(gè)特定的TCP套接字選項(xiàng)——SO_REUSEADDR。這個(gè)選項(xiàng)允許將多個(gè)socket綁定到同一個(gè)本地終端。我們建立socket的時(shí)候只要加上這么一行:
setsockopt(socket, SOL_SOCKET, SO_REUSEADDR, &flag, len) ; //C++就這么做
_Client.SetSocketOption(SocketOptionLevel.Socket, SocketOptionName.ReuseAddress, True) '這是vb.net 更加簡(jiǎn)單
知道上面的知識(shí)就很好辦了,下面我來(lái)說(shuō)說(shuō)TCP協(xié)議的穿透流程:
機(jī)器布局還是和上面使用UDP的一樣?,F(xiàn)在假設(shè)客戶A想和客戶B建立TCP連接。
首先還是 AB分別和服務(wù)器S分別建立連接,S記錄AB的互聯(lián)網(wǎng)實(shí)際終端。然后S分別向AB發(fā)送對(duì)方的實(shí)際終端。接著,從A和B向S連接時(shí)使用的端口,AB都異步調(diào)用connect函數(shù)連接對(duì)方的實(shí)際終端(就是S告訴的終端),同時(shí),AB雙方都在同一個(gè)本地端口監(jiān)聽到來(lái)的連接(也可以先監(jiān)聽,再connect更好)。由于雙方都向?qū)Ψ桨l(fā)送了connect請(qǐng)求(假設(shè)各自的SYN封包已經(jīng)穿過(guò)了自己的NAT),因此在對(duì)方connect請(qǐng)求到達(dá)本地的監(jiān)聽端口時(shí),路由器會(huì)認(rèn)為這個(gè)請(qǐng)求是剛剛那個(gè)connect會(huì)話的一部分,是已經(jīng)被許可的,本地監(jiān)聽端口就會(huì)用SYN-ACK響應(yīng),同意連接。這樣,TCP穿透NAT的點(diǎn)對(duì)點(diǎn)連接就成功了。
下面是示例代碼下載,VB.NET代碼,演示如何用TCP協(xié)議穿透NAT實(shí)現(xiàn)文件傳送,請(qǐng)用vs2005打開解決方案
http://dl2.csdn.net/down4/20070724/24133943521.rar
代碼中有一個(gè)我自己封裝的模仿vb6 winsock的控件ZXMSocket,這個(gè)socket可以讓你設(shè)置是否使用SO_REUSEADDR參數(shù),socket是事件驅(qū)動(dòng)的。
如果你要測(cè)試代碼,需要使用一個(gè)bat來(lái)啟動(dòng)發(fā)送和接收程序(文件格式請(qǐng)參照bin/Debug文件夾下的run.bat文件),這個(gè)bat的功能是以命令行的方式告訴程序登錄服務(wù)器縮使用的用戶名,對(duì)于服務(wù)器來(lái)說(shuō),這個(gè)用戶名必須是唯一的,當(dāng)然,這可能有點(diǎn)不科學(xué),但是這畢竟只是一個(gè)demo。
該文章在 2014/2/7 12:31:51 編輯過(guò)