P2P简介
P2P 是区块链网络中节点通信的基础协议,两个通信节点在两个独立的网络内部,在不清楚对方公网IP的情况下,需要通过一个第三方服务器来实现P2P通信,如图所示。
可以联想这个Server S就是那些虚拟组网的服务器,比如ZeroTier的
A和B是分别处于两个局域网内的主机,二者进行公网访问时都是通过路由器的 NAT 技术映射了一个公网地址 (IP+端口),对于A和B来说,A和B 默认情况下并不清楚对方的公网地址。因此想要通信时,必须借助一个第三方服务器 S(公网地址是公开的)。当A和B分别请求与服务器S 进行连接时,S 同时获得了 A和B 映射后的公网地址,此时 S 再将A和B 的公网地址分别传递给两方,A和B就知道彼此的地址了。
A和B是否可以直接通信了呢?是的,可以了,不过有一个小细节,对于路由器来说,它们除了负责公网地址的映射外,还有一个职责是对网络内的用户安全负责。当有一个陌生的地址想要发信息给内部主机时,路由器通常都是拒绝的,也就是说如果该地址没有在路由器内部登记注册,路由器会认为该地址存在风险,直接将网络包丢弃。
这也解释了后面为什么要先发一个包,为的就是让对方的地址在路由器里记录下来
A或B主动发出消息时,会在路由内记录对应的公网地址,正因如此A和B想要建立P2P通信时,需要分别向对方发送一次请求,然后才可建立 P2P 连接。总结一下P2P网络的通信过程如下。
- 主机 A 向服务器S 发出连接请求,S 获得 A 主机的公网地址。
- 主机 B 向服务器 S 发出连接请求,S 获得 B 主机的公网地址。
- S将A 地址发送给 B,将B 地址发送给A,此后S可以断开与A和B的连接。
- A向B发送一个消息,此消息会被 B所在路由器丢弃。
- B向A 发送一个消息,由于上一步 A 发送时,B 地址已经处于A 所在路由器列表中因此可以成功发送。
- B 发送成功后,B 所在路由器内部也记录了 A 的地址,双方可以正常通信。
服务端代码
package main
import (
"fmt"
"net"
"time"
)
func main() {
//1.服务器启动侦听
listener, _ := net.ListenUDP("udp", &net.UDPAddr{Port: 9527})
defer listener.Close()
fmt.Println("begin server at ", listener.LocalAddr().String())
//定义切片存放2个udp地址
peers := make([]*net.UDPAddr, 2, 2)
buf := make([]byte, 256)
//2. 接下来从2个UDP消息中获得连接的地址A和B
n, addr, _ := listener.ReadFromUDP(buf)
fmt.Printf("read from<%s>:%s\n", addr.String(), buf[:n])
peers[0] = addr
n, addr, _ = listener.ReadFromUDP(buf)
fmt.Printf("read from<%s>:%s\n", addr.String(), buf[:n])
peers[1] = addr
fmt.Println("begin nat \n")
//3. 将A和B分别介绍给彼此
listener.WriteToUDP([]byte(peers[0].String()), peers[1])
listener.WriteToUDP([]byte(peers[1].String()), peers[0])
//4. 睡眠10s确保消息发送完成,可以退出历史舞台
time.Sleep(time.Second * 10)
}
客户端代码
// client.go
package main
import (
"bufio"
"fmt"
"log"
"net"
"os"
"strconv"
"strings"
)
// 解析地址函数,格式为(ip:port)
func parseAddr(addr string) net.UDPAddr {
t := strings.Split(addr, ":")
port, _ := strconv.Atoi(t[1])
return net.UDPAddr{
IP: net.ParseIP(t[0]),
Port: port,
}
}
func main() {
if len(os.Args) < 5 {
fmt.Println("./client tag remoteIP remotePort port")
return
}
port, _ := strconv.Atoi(os.Args[4])
tag := os.Args[1]
remoteIP := os.Args[2]
remotePort, _ := strconv.Atoi(os.Args[3])
//一定要绑定固定端口,否则介绍人不好介绍
localAddr := net.UDPAddr{Port: port}
//与服务器建立联系(严格意义上,UDP不能叫连接)
conn, err := net.DialUDP("udp", &localAddr, &net.UDPAddr{IP: net.ParseIP(remoteIP), Port: remotePort})
if err != nil {
log.Panic("Failed ot DialUDP", err)
}
//自我介绍,亮明身份,但其实说啥都行
conn.Write([]byte("我是:" + tag))
buf := make([]byte, 256)
//从服务器获得目标地址
n, _, err := conn.ReadFromUDP(buf)
if err != nil {
log.Panic("Failed to ReadFromUDP", err)
}
conn.Close()
toAddr := parseAddr(string(buf[:n]))
fmt.Println("获得对象地址:", toAddr)
//两个人建立P2P通信
p2p(&localAddr, &toAddr)
}
func p2p(srcAddr *net.UDPAddr, dstAddr *net.UDPAddr) {
//1. 请求与对方建立联系
conn, _ := net.DialUDP("udp", srcAddr, dstAddr)
//2.发送打洞消息
conn.Write([]byte("打洞消息\n"))
//启动一个goroutine监控标准输入
go func() {
buf := make([]byte, 256)
for {
//接收UDP消息并打印
n, _, _ := conn.ReadFromUDP(buf)
if n > 0 {
fmt.Printf("收到消息:%sp2p>", buf[:n])
}
}
}()
//接下来监控标准输入,发送给对方
reader := bufio.NewReader(os.Stdin)
for {
fmt.Printf("p2p>")
//读取标准输入,以换行为读取标志
data, _ := reader.ReadString('\n')
conn.Write([]byte(data))
}
}
项目搭建
把client.go和server.go放在p2p文件夹下,然后go mod init p2p一下(p2p可以换别的名字,无所谓)
root@zlucelia:/mnt/f/wsl_go_workspace# tree p2p
p2p
├── client.go
├── go.mod
└── server.go
0 directories, 3 files
测试
-
先启动服务端:
root@zlucelia:/mnt/f/wsl_go_workspace/p2p# go run server.go begin server at [::]:9527 read from<127.0.0.1:8091>:我是:user1 read from<127.0.0.1:8092>:我是:user2 begin nat -
启动一个客户端
root@zlucelia:/mnt/f/wsl_go_workspace/p2p# go run client.go user1 localhost 9527 8091 获得对象地址: {127.0.0.1 8092 } p2p>收到消息:nih p2p> -
启动另外一个客户端
root@zlucelia:/mnt/f/wsl_go_workspace/p2p# go run client.go user2 localhost 9527 8092 获得对象地址: {127.0.0.1 8091 } p2p>收到消息:打洞消息 p2p>