TCP打洞,实现P2P(附Node源码)

本文详细解释了TCP打洞的过程,包括与UDP的对比,强调了TCP的连接性质导致的步骤调整。作者还讨论了优化策略,如连接信息的传输方式和NAT类型的考虑。同时批评了网络资源中理论与实践脱节的问题。
摘要由CSDN通过智能技术生成

前言

之前有过一篇关于 NAT打洞 的,主要是 UDP打洞,感兴趣的可以看之前的文章:NAT打洞
该篇文章主要讲述的是对 TCP打洞 的实现,以及其中的步骤的讲解和优化
也算是继续完善这个系列的坑…

可运行的代码放在了 Github仓库

当你开始阅读时,默认读者已具备一定的网络基础以及对NAT有基本的认识
那我们开始!

TCP 打洞

与 UDP 的区别

TCP与UDP打洞,最大的不同在于TCP是有连接,UDP是无连接
这就决定了,UDP可以一边打洞一边监听端口
其次就是,UDP打洞成功就能进行通信,TCP是要想办法达成握手,三次握手成了才可以通信

但其实,并没什么影响。。。因为打洞的原理并没有变,只是流程上需要稍微改变一下

流程

整个的原理和流程如下图1:(先说完这个图再说改进)

TCP打洞流程

  • 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:JNI: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:YNR: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 都可以

改进后:(其实也没多大必要)

I NI S NR R Step2: TCP - S:Z < NR:K < R:J NR:K;想要被 I 连接 Step1: TCP - I:X > NI:Y > S:Z NI:Y;想要连接R par NI:Y;需要打洞 Step3: TCP - NI:Y < NR:K < R:J (打洞) Step4: ① Destroy TCP R:J ② CreateServer Listen R:J Step5: New Connection S:* < NR:* < R:* 打洞完成 NR:K Step6: Destroy TCP I:X Step7: TCP - I:X Step7: TCP - I:X > NI:Y > NR:K > R:J ACK ACK I NI S NR R
  • par 里的 Step1、Step2 不分先后
  • Step3 发出去就可以进入 Step4 了
  • Step5 的 * 号表示任意,只要不是 R:J 都行(反正你也开Server监听了,也绑定不了)
  • Step7 是用 Step6 的 I:XNR: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()
    }))
})

其他

一些实验发现

  1. NAT 维持 TCP洞口 的时间一般比较短,我试过我这边区域的网络,一般是维持 5s 左右,超过 5s 这个端口没有数据包,
    NAT 会默认这个端口不再使用,外面数据包再想进来都会被 NAT 丢弃,这点不管是打洞,亦或是已经建立的TCP通信
    (通过服务端设置延迟返回信息,测试得到,但各区域的 NAT 会有所不同,可以自己试下)

  2. 同样,在这段维持时间内,用户通过 同一个端口 向外发送信息,都会绑定同一个 NAT 端口。所以并不需要端口复用,
    只要在维持时间内,客户端 Destroy 之前的TCP连接(再暴力点可以直接抛出来终结掉之前的连接),
    再用 同一个端口 建立连接/监听这个端口 都是可以

    数据包发送是毫秒级的,对于 5s 的窗口期来说 是完全足够的 (手动才勉强)

  3. 当前这个,最差可以在 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 建立连接,需要 端口预测 / 端口探测,先挖个坑,后续有时间再来填…


参考文献


  1. TCP Connections for P2P Apps: A Software Approach to Solving the NAT Problem ↩︎

  • 22
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
要使用 TCP 连接实现 P2P (点对点) 通信,你可以使用 Node.js 内置的 `net` 模块。 以下是一些实现步骤: 1. 首先,你需要创建一个 TCP 服务器,让其他节点可以连接到它。你可以使用 `net.createServer()` 方法来创建一个服务器,该方法接受一个回调函数作为参数,该回调函数将在每次有新连接时被调用。 ``` const net = require('net'); const server = net.createServer((socket) => { console.log('New client connected'); }); ``` 2. 接下来,你需要让服务器开始监听传入的连接。你可以使用 `server.listen()` 方法来做到这一点,该方法接受一个端口号和一个可选的回调函数作为参数。 ``` server.listen(3000, () => { console.log('Server started on port 3000'); }); ``` 3. 现在,你需要创建一个 TCP 客户端,它将连接到服务器。你可以使用 `net.connect()` 方法来创建一个客户端连接,该方法接受一个端口号和一个 IP 地址作为参数。 ``` const client = net.connect({ port: 3000, host: 'localhost' }, () => { console.log('Connected to server'); }); ``` 4. 当客户端连接到服务器时,服务器的回调函数将被调用,并且你可以在其中执行任何你想要的操作,例如将客户端添加到一个数组中以便稍后向它发送消息。 ``` const clients = []; const server = net.createServer((socket) => { console.log('New client connected'); clients.push(socket); // Handle incoming data from the client socket.on('data', (data) => { console.log(`Received data from client: ${data}`); }); }); ``` 5. 最后,你可以使用 `socket.write()` 方法将消息发送到连接到服务器的某个客户端。该方法接受一个字符串或一个缓冲区作为参数。 ``` clients[0].write('Hello, client!'); ``` 以上是使用 TCP 连接实现 P2P 通信的基本步骤,你可以根据你的具体需求进行调整和扩展。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值