p2p不同内网环境如何通信
P2P
常规CS
架构和P2P
架构图:

这里就有两个问题了:
- 现在国内终端基本都是内网下,没有独立的公网
IP
,如何peer
和peer
如何通信 - 怎么发现
peer
,仍然需要一个中心服务器,或者别的什么东西
NAT类型
注:该部分内容完全来自:NAT 原理以及 UDP 穿透
1. 完全锥型
从同一个内网地址端口(192.168.1.1:7777
)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000
),192.168.1.1:7777
可以收到任意外部主机发到 1.2.3.4:10000
的数据报。

2.受限锥型
受限锥型也称地址受限锥型,在完全锥型的基础上,对 ip 地址进行了限制。
从同一个内网地址端口(192.168.1.1:7777
)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000
),其访问的服务器为 8.8.8.8:123
,只有当 192.168.1.1:7777
向 8.8.8.8:123
发送一个报文后,192.168.1.1:7777
才可以收到 8.8.8.8
发往 1.2.3.4:10000
的报文。

3.端口受限锥型
在受限锥型的基础上,对端口也进行了限制。
从同一个内网地址端口(192.168.1.1:7777
)发起的请求都由 NAT 转换成公网地址端口(1.2.3.4:10000
),其访问的服务器为 8.8.8.8:123
,只有当 192.168.1.1:7777
向 8.8.8.8:123
发送一个报文后,192.168.1.1:7777
才可以收到 8.8.8.8:123
发往 1.2.3.4:10000
的报文。

4.对称型
在 对称型NAT 中,只有来自于同一个内网地址端口 、且针对同一目标地址端口的请求才被 NAT 转换至同一个公网地址端口,否则的话,NAT 将为之分配一个新的公网地址端口。
如:内网地址端口(192.168.1.1:7777
)发起请求到 8.8.8.8:123
,由 NAT 转换成公网地址端口(1.2.3.4:10000
),随后内网地址端口(192.168.1.1:7777
)又发起请求到 9.9.9.9:456
,NAT 将分配新的公网地址端口(1.2.3.4:20000
)

UDP打洞技术有一个主要的条件:只有当两个NAT都是Cone NAT(或者非NAT的防火墙)时才能工作。
中继
类似frp
UDP打洞
PC1(192.168.1.1:7777)
发送 UDP 请求到Server(9.9.9.9:1024)
,此时 Server 可以获取到 PC1 的出口地址端口(也就是 Router1 的出口地址端口)1.2.3.4:10000
,同时 Router1 添加一条映射192.168.1.1:7777 <=> 1.2.3.4:10000 <=> 9.9.9.9:1024
PC2(192.168.2.1:8888)
同样发送 UDP 请求到 Server,Router2 添加一条映射192.168.2.1:8888 <=> 5.6.7.8:20000 <=> 9.9.9.9:1024
Server 将 PC2 的出口地址端口(
5.6.7.8:20000
) 发送给 PC1Server 将 PC1 的出口地址端口(
1.2.3.4:10000
) 发送给 PC2PC1 使用相同的内网地址端口(
192.168.1.1:7777
)发送 UDP 请求到 PC2 的出口地址端口(Router2 5.6.7.8:20000
),此时 Router1 添加一条映射192.168.1.1:7777 <=> 1.2.3.4:10000 <=> 5.6.7.8:20000
,与此同时 Router2 没有关于1.2.3.4:10000
的映射,这个请求将被 Router2 丢弃PC2 使用相同的内网地址端口(
192.168.2.1:8888
)发送 UDP 请求到 PC1 的出口地址端口(Router1 1.2.3.4:10000
),此时 Router2 添加一条映射192.168.2.1:8888 <=> 5.6.7.8:20000 <=> 1.2.3.4:10000
,与此同时 Router1 有一条关于5.6.7.8:20000
的映射(上一步中添加的),Router1 将报文转发给PC1(192.168.1.1:7777)
在 Router1 和 Router2 都有了对方的映射关系,此时 PC1 和 PC2 通过 UDP 穿透建立通信。

UDP打洞golang代码
NAT类型测试

python 服务端
import socket
import time
PORT = 9092
server_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
address = ("0.0.0.0", PORT)
server_socket.bind(address)
server_socket.settimeout(300)
client_a_b_info = []
def transfer_info():
server_socket.sendto(bytes(("client connected#"+client_a_b_info[1][0]+":"+str(client_a_b_info[1][1])+"\n").encode('utf-8')), client_a_b_info[0])
time.sleep(1)
server_socket.sendto(bytes(("client connected#"+client_a_b_info[0][0]+":"+str(client_a_b_info[0][1])+"\n").encode('utf-8')), client_a_b_info[1])
time.sleep(1)
print("连接成功,10s后服务端退出")
time.sleep(10)
while True:
receive_data, client = server_socket.recvfrom(1024)
time.sleep(1)
print("recv udp data: ", receive_data)
if b"client_upload_info" in receive_data:
client_a_b_info.append(client)
if len(client_a_b_info) == 2:
transfer_info()
break
测试:
服务端:
$ python3 server.py
recv udp data: b'client_upload_info\n'
recv udp data: b'client_upload_info\n'
连接成功,10s后服务端退出
测试:
$ nc -nu 127.0.0.1 9092
client_upload_info
client_upload_info
client_upload_info
client_upload_info
client connected#127.0.0.1:6840
client connected#127.0.0.1:6840
Golang客户端
基本就是上面步骤里面的实现:
package udp_punch_hole
import (
"fmt"
"net"
"strconv"
"strings"
"time"
)
var serverIp = net.ParseIP("1.2.3.4")
var addr = &net.UDPAddr{IP: serverIp, Port: 9092}
func sendUDPMsg(msg string, conn *net.UDPConn) error {
_, err := conn.Write([]byte(msg))
return err
}
func recvAnotherPeerInfo(serverConn *net.UDPConn) (conn *net.UDPConn, err error) {
localAddrStr := serverConn.LocalAddr().String()
//localAddrIp := net.ParseIP(strings.Split(localAddrStr, ":")[0])
localAddrPort, err := strconv.Atoi(strings.Split(localAddrStr, ":")[1])
localAddr := &net.UDPAddr{IP: net.IPv4zero, Port: localAddrPort}
fmt.Printf("localAddr: %+v\n", localAddr)
if err != nil {
return nil, err
}
data := make([]byte, 1024)
for {
n, _, err := serverConn.ReadFromUDP(data)
if err != nil {
return nil, err
}
fmt.Printf("RECV: %v\n", string(data[:n]))
peerInfo := strings.Split(string(data[:n]), "#")[0]
peerAddrStr := strings.TrimSpace(strings.Split(string(data[:n]), "#")[1])
if peerInfo != "client connected" {
continue
}
println("对方ip:addr ", peerAddrStr)
println("自己ip: ", localAddr.String())
peerIp := strings.Split(peerAddrStr, ":")[0]
peerPort, _ := strconv.Atoi(strings.Split(peerAddrStr, ":")[1])
var peerAddr = &net.UDPAddr{IP: net.ParseIP(peerIp), Port: peerPort}
serverConn.Close()
udpConn, err := net.DialUDP("udp", localAddr, peerAddr)
if err != nil {
println(": ", err.Error())
return nil, err
}
err = sendUDPMsg("for trust usage", udpConn)
if err != nil {
return nil, err
}
return udpConn, nil
}
}
func transferMsg(udpConn *net.UDPConn) {
time.Sleep(1 * time.Second)
go func() {
data := make([]byte, 1024)
for {
println("waiting...")
n, _, err := udpConn.ReadFromUDP(data)
println("done....")
if err != nil {
return
}
print("recv: ", string(data[:n]))
}
}()
for {
err := sendUDPMsg("test\n", udpConn)
if err != nil {
println(err.Error())
} else {
println("发送成功:", udpConn.RemoteAddr().String())
}
time.Sleep(3 * time.Second)
}
}
func udp_hole() error {
udpConn, _ := net.DialUDP("udp", nil, addr)
err := sendUDPMsg("client_upload_info.haha", udpConn)
if err != nil {
println(err.Error())
return err
}
in, err := recvAnotherPeerInfo(udpConn)
if err != nil {
println(err.Error())
return err
}
transferMsg(in)
return nil
}
不同主机测试:
func Test_udp_hole(t *testing.T) {
udp_hole()
}
结果:
=== RUN Test_udp_hole
localAddr: 0.0.0.0:52952
RECV: client connected#202.107.195.215:59791
对方ip:addr 202.107.195.215:59791
自己ip: 0.0.0.0:52952
waiting...
发送成功: 202.107.195.215:59791
done....
recv: for trust usagewaiting...
done....
recv: test
waiting...
发送成功: 202.107.195.215:59791
done....
recv: test
waiting...
发送成功: 202.107.195.215:59791
done....
recv: test
waiting...
发送成功: 202.107.195.215:59791
遇到的问题
想偷个懒,直接一个主机跑,收不到数据:
func Test_udp_hole(t *testing.T) {
go udp_hole()
time.Sleep(time.Second * 3)
go udp_hole()
time.Sleep(time.Hour)
}
UDP打洞为何不能一个主机下?或者我代码里一个主机下为何不能打洞
没找到原因,TODO