一、初识NAT NAT 类型 NAT 分为四种类型,分别为 NAT1234 🙃
NAT1 - 完全锥形NAT(Full Clone NAT)
NAT2 - 限制性锥形NAT(Restricted Clone NAT)
NAT3 - 端口限制性锥形NAT( Port Restricted Clone NAT)
NAT4 - 对称式NAT(Symmetric NAT)
怎么判断呢,先从 绿色 箭头看起。
现在假设
192.168.0.100 通过 8000 端口 访问 114.135.246.90 的 9001 端口
经过NAT之后,会形成这样一种结果:
内网的 192.168.0.100:8000 ↔ 120.230.117.10:9999 NAT公网IP,形成绑定关系
服务端 114.135.246.90:9001 被 120.230.117.10:9999 NAT公网IP,访问
然后如果: 8000 端口 能收到 紫色 发来的消息,就是 NAT1 8000 端口 收不到 紫色 发来的消息,却能收到 浅蓝色 线发来的消息,就是 NAT2 8000 端口 收不到 紫色 和 浅蓝色 线发来的消息,只能收到 绿色 线返回来的消息,就是 NAT3 | NAT4
NAT3 和 NAT4 的区别: NAT3 是 8000 端口与 9999 端口形成绑定关系,不管通过 8000 端口访问谁,·都是从 9999 端口出去 NAT4 是 8000 端口与 9999 端口形成的绑定关系是有条件的,8000 端口访问同一
ip:port 时,都会从 9999 端口出去,但通过 8000 端口访问其他 ip:port 时,就会绑定 NAT 其他端口,比如 9990
我这移动大内网,一般都是NAT3,手机开数据流量,基本都是NAT4
NAT 打洞原理 打洞,说白了就是让 NAT 留下一条访问记录 谁来打洞,就在谁的 NAT 上,留下一条对方的记录,这样在对方访问自己的时候,不会因为自己的 NAT 因为缺少记录而把数据包丢弃
( 对于NAT来说,因为没记录,所以也不知道转发给内网的谁,就算知道 如果没有记录,则数据包的来源也不一定合法 )
二、UDP打洞 看图
这里我们通过一个服务端,然后让A B两个内网主机进行UDP打洞并通信。适用于 NAT123
说明: A、B 分别为两个不同内网下的主机 NA、NB分别问 A 和 B 的 NAT 出口 中间的 90.20.88.99
为服务端主机,下方称为 S
图解:
首先我们让 A:8001 与 S:9001 建立UDP通信 此时 NA 就会留下 内网的 A:8001 ↔ NA:3333 的绑定,NA:3333 对 S:9001 的访问 即:A:8001 - NA:3333 - S:9001
接着让 B 也执行同样的操作 此时 NB 就会留下 内网的 B:6000 ↔ NB:4444 的绑定,NB:4444 对 S:9001 的访问 即:B:6000 - NB:4444 - S:9001
此时 S 知道 NA 和 NB ,且还知道 (看着上面的两个‘即’ 继续往下看) 与 A 通信可以用 S:9001 向 NA:3333 发送消息 与 B 通信可以用 S:9001 向 NB:4444 发送消息 若让 A - B 直接通信,得让 A 知道 NB ,B 知道 NA 所以 将 NB:4444 通过 S:9001 发送给 NA:3333 , A 知道可以通过 NB:4444 向 B 通信 将 NA:3333 通过 S:9001 发送给 NB:4444 , B 知道可以通过 NA:3333 向 A 通信
若此时 B:6000 - NB:4444 直接向 NA:3333 进行 UDP 通信 NA 会因为不存在 A:8001 - NA:3333 - NB:4444 这条记录,而将数据包丢弃 (同理 A 奕是如此,此时双方是对等的) 但此时,在 NB 中则会留下 B:6000 - NB:4444 - NA:3333 这样一条记录,这一步就是所谓的 “打洞”
NB:4444 向 NA:3333 发送完之后,此时 A:8001 - NA:3333 再向 NB:4444 发送 UDP 数据包时 因为 NB 有了 B:6000 - NB:4444 - NA:3333 这样一条记录,所以会将 NA:3333 发来的数据包转发到 B:6000 中 并且此时在 NA 也会留下 A:8001 - NA:3333 - NB:4444 这条记录
之后 因为 NA 有: A:8001 - NA:3333 - NB:4444 A:8001 - NA:3333 - S:9001 NB 有: B:6000 - NB:4444 - NA:3333 B:6000 - NB:4444 - S:9001 所以 A 和 B 都可以与 S 直接通信,A 与 B 也可以直接通信
看码 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 const dgram = require ("dgram" );const option = process.argv .splice (2 )let localPORT = 6677 const SERVER_PORT = option[0 ] || 9001 const SERVER_ADDR = option[1 ] || "114.135.246.90" let localInfo = "" let serverInfo = "" let timeout = setInterval (() => { createClient () }, 1000 ) let getConnect = (client ) => { console .log (`与${serverInfo} 连接成功。` ); client.send ("" ) } let getMessage = (client, data, rinfo ) => { console .log (`[${rinfo.address} :${rinfo.port} ]:${data.toString()} ` ) let res = JSON .parse (data.toString ()) if (res.msg == "wait" ) { } if (res.msg == "toClient" ) { let second = res.second createNewClient (second.addr , second.port ) client.close () } if (res.msg == "toServer" ) { let first = res.first createServer (first.addr , first.port ) client.close () } } function createClient ( ) { const client = dgram.createSocket ("udp4" ); try { client.bind (localPORT, "" , () => { clearInterval (timeout) }) client.connect (SERVER_PORT , SERVER_ADDR ) client.on ('connect' , () => getConnect (client)) client.on ('message' , (data, rinfo ) => { getMessage (client, data, rinfo) }) client.on ("listening" , (res ) => { localInfo = `[${client.address().address} :${client.address().port} ]` serverInfo = `[${SERVER_ADDR} :${SERVER_PORT} ]` console .log (`${localInfo} - ${serverInfo} ` ); }) client.on ("close" , (res ) => { }) client.on ("error" , (err ) => { console .log ("error" , "发生一个错误" ); console .log ("正在重连..." ); localPORT++ }) } catch (error) { console .log ("错啦!!!" ) console .log (error) } } function createNewClient (serverAddr, serverPort ) { const client = dgram.createSocket ("udp4" ); try { client.bind (localPORT) client.connect (serverPort, serverAddr) client.on ('connect' , () => { console .log (`与${serverInfo} 连接成功。` ); client.send (`${localInfo} ` ) console .log ("回车发送消息:" ); process.stdin .on ('data' , data => { let sendData = data.toString ().trim () console .log (`${localInfo} : ${sendData} ` ); client.send (sendData); }) }) client.on ('message' , (data, rinfo ) => { console .log (`[${rinfo.address} :${rinfo.port} ]: ${data.toString()} ` ) }) client.on ("listening" , (res ) => { localInfo = `[${client.address().address} :${client.address().port} ]` serverInfo = `[${serverAddr} :${serverPort} ]` console .log (`${localInfo} - ${serverInfo} ` ); }) client.on ("close" , (res ) => { console .log ("close" , res); }) client.on ("error" , (err ) => { console .log ("error" , "发生一个错误" ); console .log ("正在重连..." ); localPORT++ }) } catch (error) { console .log ("错啦!!!" ) console .log (error) } } function createServer (clientAddr, clientPort ) { const server = dgram.createSocket ('udp4' ) server.bind (localPORT) server.send ("" , clientPort, clientAddr) server.send ("ready" , SERVER_PORT , SERVER_ADDR ) server.on ("listening" , () => { console .log (`start to listening: ${server.address().address} :${server.address().port} ` ) console .log ("回车发送消息:" ); process.stdin .on ('data' , data => { let sendData = data.toString ().trim () console .log (`${localInfo} : ${sendData} ` ); server.send (sendData, clientPort, clientAddr); }) }) server.on ("connect" , (res ) => { console .log ("connect:" , res); }) server.on ("message" , (msg, rinfo ) => { console .log (`[${rinfo.address} :${rinfo.port} ]: ${msg.toString()} ` ) let message = msg.toString () }) server.on ("close" , (res ) => { console .log ("close" , res); }) server.on ("error" , (err ) => { console .log ("error" , err); }) }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 const dgram = require ('dgram' )const server = dgram.createSocket ('udp4' )let first = "" let second = "" let alive = {}server.bind (9001 ) server.on ("listening" , () => { console .log (`start to listening: ${server.address().address} :${server.address().port} ` ) }) server.on ("connect" , (res ) => { console .log ("connect:" , res); }) server.on ("message" , (message, rinfo ) => { let msg = message.toString () console .log (`[${rinfo.address} :${rinfo.port} ]:${msg.toString()} ` ) let count = Object .keys (alive).length if (count == 0 ) { first = { addr : rinfo.address , port : rinfo.port } server.send (`{"res": "wait"}` , rinfo.port , rinfo.address ) } else if (count == 1 ){ second = { addr : rinfo.address , port : rinfo.port } let res = { first, msg : "toServer" } server.send (JSON .stringify (res), rinfo.port , rinfo.address ) } alive[`${rinfo.address} :${rinfo.port} ` ] = { addr : rinfo.address , port : rinfo.port } console .log (alive) if (msg == "ready" ) { let res = { second, msg : "toClient" } server.send (JSON .stringify (res), first.port , first.addr ) alive = {} } }) server.on ("close" , (res ) => { console .log ("close" , res); }) server.on ("error" , (err ) => { console .log ("error" , err); })
这是个小的 demo,分别对应 client.js
和 server.js
A 和 B 通用 client.js
,S 部署 server.js
即可
用的时候先在 S 启动 server.js
然后分别在 A B 启动 client.js
即可,连接成功后 若将 S 中的 server.js
停掉,A B仍能正常通信,则试验成功
三、TCP打洞
TCP打洞,打洞的原理其实还是那个原理,就是在NAT上留下记录。
TCP的过程就不细说了,但是TCP打洞会比UDP打洞困难得多 (因为我也没成功)
主要是因为,TCP和UDP不同:
UDP是无链接,同一个端口既可以发送 同时可以监听接收。同一个端口,可以给很多人发送,也可以接收很多人发来的信息
但TCP是套接字,自己的 IP:PORT 是与对方的 IP:PORT 是绑定的,期间端口处于被占用的状态,只能发送或者是收到response回来的信息,不能直接从 发送状态 改为 监听状态,需要先进行断开 或者 设置为端口复用
但在端口复用的过程中,NAT可能会因此而改变端口,其原因可能是因为处于 TIME_WAIT 状态,又或是像 NAT4 一样处理新的TCP 连接
所以 此时想要通过TCP打洞,建立可靠的TCP连接,这时候就要搬出 TCP 状态迁移图 了 这个时候,想要提高成功打通的概率,可以让双方互发SYN,以达到 同时打开 的效果 因为一个端口发送失败,可能会导致这个端口在短时间内不能再使用
所以我想到的一个方法就是: 先让 A 向 S 发送正常的 TCP 连接,让 S 知道了 A-NA 的 TCP 端口 然后 S 向 NA 端口 往后的 10个端口 同时循环发送 SYN,A 也向 S 发送10+次 SYN,这样就有极大地概率命中其中一个端口 只要 NA 放行了一个任意一个 S 发来的 SYN,就能进行完整握手,建立起TCP连接通信
当然啦,如果这里的 S 是公网IP,则 A 向 S 建立的连接都可以连上这里主要是想实现的是 S 向 A 主动建立起的连接 因为如果 S 能主动向A建立连接,S 就可以换成 B-NB 最终的效果其实就是 NA 和 NB 相互不断发送 SYN,以同时打开 NA 和 NB 的通道 建立连接
四、结语 最后再说两句吧… 后来才知道,原来这就是叫 p2p…..好吧是我浅薄了
然后关于UDP打洞的部分,我给我的小伙伴讲,给他讲完了之后 他说了一句:“你这是在建立 UDP 的可靠连接嘛”,然后两个人看着图都沉默了……
然后TCP部分,我确实没能在两个大NAT网上打通,所以我也好奇和请教一下大佬们,有什么好的解决方案实现TCP打洞和建立连接的呢 因为如果光用UDP打洞实现连接之后,简单的通信还好,但如果是用来传输文件就好像不太现实?还要手写校验和重传机制?还要注意MTU、数据包大小?
或者像酷狗、迅雷这种利用客户端的,又是怎么实现的呢
如果可以的话,能不能有个 node.js 的 demo,C语言真的好难顶…
另外: 如果觉得颜色有点花里胡哨的话,之后会在这 NAT打洞 同步更新一篇吧
完
参考文章