前言
之前有过一篇关于 NAT打洞 的,主要是 NAT介绍 以及 UDP打洞,感兴趣的可以看之前的文章:NAT打洞
该篇文章主要讲述的是对 TCP打洞 的实现,以及其中的步骤的讲解和优化
也算是继续完善这个系列的坑…
可运行的代码放在了 Github仓库
过来填坑啦!下一篇: TCP实现P2P (NAT3-NAT4)
当你开始阅读时,默认读者已具备一定的网络基础以及对NAT有基本的认识
那我们开始!
TCP 打洞
与 UDP 的区别
TCP与UDP打洞,最大的不同在于TCP是有连接,UDP是无连接
这就决定了,UDP可以一边打洞一边监听端口
其次就是,UDP打洞成功就能进行通信,TCP是要想办法达成握手,三次握手成了才可以通信
但其实,并没什么影响。。。因为打洞的原理并没有变,只是流程上需要稍微改变一下
流程
整个的原理和流程如下图1:(先说完这个图再说改进)
- Step1-2: R 向服务器注册信息(这里其实UDP和TCP都可以,改进部分有说明)
- Step3: I 向服务器发出 “想要连接的请求”,用的是TCP(这样可以知道 I 自身的 NAT 信息 NI:Y)
- Step4: 服务器将 “I 想要连接的请求” 发送给 R
- Step5: R 收到服务器发来的 “I 想要连接的请求” 后,用 R:J 向服务器发起 TCP 连接
服务器收到连接后,会收到来自 NR:K 的信息,之后会判断 NR 是否等于 R,- 是的话,即 R 不经过 NAT,I 可以直接发起连接
- 否的话,即 R 经过 NAT,需要 R 打洞后,I 才可以发起连接(这里讨论这种情况)
- Step6: 服务器向 R 发送打洞的消息,并将 NI:Y 信息发送给 R,表示可以通过 NI:Y 与 I 通信
- Step7: R 收到 NI:Y 后,用 R:J 向 NI:Y 发送 TCP 连接请求(指SYN)
- Step8: R 不会收到返回信息(指ACK),但这里也有可能收到拒绝信息,但这里不影响
随后 R 直接断开通过 R:J 发起的 TCP 连接,并开启 TCP Server 监听 J 端口 - Step9: R 用 其他端口 向服务器发送 “打洞完成” 的信息
- Step10: 服务器通过 NI:Y 将 “打洞完成” 的信息 以及 NR:K 的信息发送给 I,并且关闭与之的通信
- Step11: I 收到信息后,会终止 NI:Y 与服务器的 TCP 连接,然后会用 NI:Y 向 NR:K 发起 TCP 连接请求(指SYN)
- 最终 SYN 会被 NR:K 送进给 R:J,之后会进行完整的三次握手建立TCP连接
改进
这里的 Step1、2、9 其实对连接并没有要求
1 2 9 可以用同一个连接 专门用来发送信息 也是可以的,这里起到的作用单纯是通知状态和交换信息
甚至 Step1-2 直接用 R:J 与服务器进行通信也是可以的,因为 Step6 之后都会断开与服务器的连接而转为打洞和监听
其次,这里并没有那么严谨的区分 I 和 R 与服务器发起连接的顺序,因为与服务器建立连接之后,不管是 Step4 还是 Step10,都是在等待服务器返回信息 才进行下一步操作(这里代码中我用了心跳包的方式维持)
Step6 的 NI:Y 信息其实可以在 Step4 就带过来给 R,这里主要是考虑是否需要考虑打洞的情况,因为 R 如果不需要打洞的话 NI:Y 的信息其实并不需要告知 R
但如果单纯是考虑是否需要打洞的话,其实还得考虑双方的 NAT 类型 来进一步判断是谁来打洞
比如说:
I 是 NAT3 端口限制性锥形NAT( Port Restricted Clone NAT),
R 是 NAT4 对称式NAT(Symmetric NAT)
则需要 I 来打洞,R 向 NI 发起连接
如果不需要考虑的话,其实这里 punchHole
一直为 True 都可以
改进后:(其实也没多大必要)
- par 里的 Step1、Step2 不分先后
- Step3 发出去就可以进入 Step4 了
- Step5 的 * 号表示任意,只要不是 R:J 都行(反正你也开Server监听了,也绑定不了)
- Step7 是用 Step6 的 I:X 向 NR:K 直接发起连接
代码部分说明
1. 使用前,请先将 Server.js 部署,记得开启端口
2. 完善 Client*.js 中的 `SERVER_IP` `SERVER_PORT` `LOCAL_PORT`
3. 启动 ClientA.js 和 ClientB.js
/**
* 这里的 from 和 to,分别代表的是:
* from: 想要发起连接的一方
* to: 被连接的一方
*
* 它 **并不关乎** 这个 msg 是由 ClientA 发出还是由 ClientB 发出
*/
msg: {
from: "ClientA",
to: "ClientB"
}
// 连接成功后,可以将 ClientA.js、ClientB.js 中的这段代码放开,然后通过键盘输入发送消息
// 键盘输入发送消息
process.stdin.on('data', data => {
socket.write(JSON.stringify({
type: "msg",
msg: data.toString().trim()
}))
})
其他
一些实验发现
-
NAT 维持 TCP洞口 的时间一般比较短,我试过我这边区域的网络,一般是维持 5s 左右,超过 5s 这个端口没有数据包,
NAT 会默认这个端口不再使用,外面数据包再想进来都会被 NAT 丢弃,这点不管是打洞,亦或是已经建立的TCP通信
(通过服务端设置延迟返回信息,测试得到,但各区域的 NAT 会有所不同,可以自己试下) -
同样,在这段维持时间内,用户通过 同一个端口 向外发送信息,都会绑定同一个 NAT 端口。所以并不需要端口复用,
只要在维持时间内,客户端Destroy
之前的TCP连接(再暴力点可以直接抛出来终结掉之前的连接),
再用 同一个端口 建立连接/监听这个端口 都是可以数据包发送是毫秒级的,对于 5s 的窗口期来说 是完全足够的 (手动才勉强)
-
当前这个,最差可以在 NAT3 和 NAT3 之间建立起连接,如果其中一方是 NAT4 的话,NAT3 作为需要打洞的一方即可
但可能需要用 同一个端口 向 NAT4 的多个端口发送数据包(端口预测)
说在最后
首先是发现,网上关于 “NAT穿越”、“打洞”、“P2P”… 等等话题,要么就是一大段理论概念,要么就是水文章,实际有用的、能参考的没多少
剩下能参考的,基本都是 UDP 的,TCP 就几乎没有
然后 TCP 吧,有的基本都是 C/C++ 的
然后还一堆不要脸 CV 的,要点脸的都知道挂个转载或者挂个出处吧
特别是那些有这个字段 SO_REUSEADDR
,还跟你说一定要设置端口复用的,一大半都是 CV 的
( 我就这么直接下定义了!)
然后这篇文章也算是补充了一下 我之前 NAT打洞 一文里 关于 TCP 的部分
当时刚接触 然后去做相关的测试和实验,还比较的懵懂,然后是找了些资料 又再去研究了下 计算机网络 ,然后发现网上的 真的 好水呀,真的好气!
然后还是决定去翻了翻书本和找了下文献之类的,现在过来填个坑吧
这同时也是完善 P2P 这部分的坑,后续其实还有
就是 NAT3 / NAT4 与 NAT4 相互连接,NAT4 一般是 手机热点 连接的网络
NAT3/4 与 NAT4 建立连接,需要 端口预测 / 端口探测,先挖个坑,后续有时间再来填…
过来填坑啦! TCP实现P2P (NAT3-NAT4)
完