前言
2024 年的 1024 过来给 2023.07 的这篇文章打几个补丁 ~O(∩_∩)O 哈哈~
如果你是 NAT 的小白,对 NAT 并不了解,这篇文章就很适合你,但其中的代码部分写的很是一般,末尾关于 TCP 部分的描述也不到位,建议仅阅读 初始 部分 以及看完几个图就好,知道大概的打洞流程即可,不建议深究和细看
若你对 NAT 有了一些初步的了解,又或是想知道怎么与 NAT 建立连接的
我强烈建议你直接阅读下一篇文章 TCP打洞,实现P2P(附Node源码)
如果你向来只打逆风局,可以直接跳转到 NAT4 的文章 TCP实现P2P(NAT3-NAT4)
一、初识NAT
1.1 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
2. NAT 打洞原理
打洞,说白了就是让 NAT 留下一条访问记录
谁来打洞,就在谁的 NAT 上,留下一条对方的记录,这样在对方访问自己的时候,不会因为自己的 NAT 因为缺少记录而把数据包丢弃
( 对于NAT来说,因为没记录,所以也不知道转发给内网的谁,就算知道 如果没有记录,则数据包的来源也不一定合法 )
二、UDP打洞
2.1 看图
这里我们通过一个服务端,然后让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 也可以直接通信
2.2 看码
// client.js
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("")
// process.stdin.on('data', data => {
// let sendData = data.toString().trim()
// client.send(sendData);
// })
}
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.send(`${localInfo}-已连接-serverInfo`,SERVER_PORT,SERVER_ADDR)
})
client.on("close", (res) => {
// console.log("close", "正在关闭");
})
client.on("error", (err) => {
console.log("error", "发生一个错误");
console.log("正在重连...");
localPORT++
// createClient()
})
} 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.send(`${localInfo}-已连接-serverInfo`,SERVER_PORT,SERVER_ADDR)
})
client.on("close", (res) => {
console.log("close", res);
})
client.on("error", (err) => {
console.log("error", "发生一个错误");
console.log("正在重连...");
localPORT++
// createClient()
})
} 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.send(message, clientPort, clientAddr)
})
server.on("close", (res) => {
console.log("close", res);
})
server.on("error", (err) => {
console.log("error", err);
})
}
// server.js
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 的通道 建立连接
具体的实现,可以看下一篇:TCP打洞,实现P2P(附Node源码)
四、结语
最后再说两句吧…
后来才知道,原来这就是叫 p2p…好吧是我浅薄了
然后关于UDP打洞的部分,我给我的小伙伴讲,给他讲完了之后
他说了一句:“你这是在建立 UDP 的可靠连接嘛”,然后两个人看着图都沉默了…
然后TCP部分,我确实没能在两个大NAT网上打通,所以我也好奇和请教一下大佬们,有什么好的解决方案实现TCP打洞和建立连接的呢
因为如果光用UDP打洞实现连接之后,简单的通信还好,但如果是用来传输文件就好像不太现实?还要手写校验和重传机制?还要注意MTU、数据包大小?
或者像酷狗、迅雷这种利用客户端的,又是怎么实现的呢
如果可以的话,能不能有个 node.js 的 demo,C语言真的好难顶…
另外:
如果觉得颜色有点花里胡哨的话,之后会在这 NAT打洞 同步更新一篇吧
过来填坑啦!
下一篇:TCP打洞,实现P2P(附Node源码)
再下一篇:TCP实现P2P (NAT3-NAT4)
完
参考文章
- P2P技术详解(三):P2P中的NAT穿越(打洞)方案详解(进阶分析篇)
- 用TCP穿透NAT(TCP打洞)的实现_mengzhengjie的博客-CSDN博客
- p2p通信原理及实现_p2p传输原理_LateLinux的博客-CSDN博客
- NAT路由器“打洞”技术,即P2P通信实现原理(非常详细)_如何实现设备和路由器之间的打洞_蜡笔小勋的博客-CSDN博客
- 简谈udp打洞和tcp打洞_upd打洞完成了 然后两台电脑如何tcp连接_Curtis Wan的博客-CSDN博客
- 为NAT后面的客户端创建Tcp连接 | 码农家园
- TCP实现P2P通信、TCP穿越NAT的方法、TCP打洞 - 知乎
- 用TCP穿透NAT(TCP打洞)的实现 - jack_Meng - 博客园
- 关于TCP打洞技术(P2P)_p2p和tcp_Sidyhe的博客-CSDN博客
- TCP打洞方法_一颗简单的心的博客-CSDN博客