Golang搭建P2P网络

556 阅读5分钟

P2P简介

P2P 是区块链网络中节点通信的基础协议,两个通信节点在两个独立的网络内部,在不清楚对方公网IP的情况下,需要通过一个第三方服务器来实现P2P通信,如图所示。

image.png

可以联想这个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网络的通信过程如下。

  1. 主机 A 向服务器S 发出连接请求,S 获得 A 主机的公网地址。
  2. 主机 B 向服务器 S 发出连接请求,S 获得 B 主机的公网地址。
  3. S将A 地址发送给 B,将B 地址发送给A,此后S可以断开与A和B的连接。
  4. A向B发送一个消息,此消息会被 B所在路由器丢弃。
  5. B向A 发送一个消息,由于上一步 A 发送时,B 地址已经处于A 所在路由器列表中因此可以成功发送。
  6. 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

测试

  1. 先启动服务端:

    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
    
  2. 启动一个客户端

    root@zlucelia:/mnt/f/wsl_go_workspace/p2p# go run client.go user1 localhost 9527 8091
    获得对象地址: {127.0.0.1 8092 }
    p2p>收到消息:nih
    p2p>
    
  3. 启动另外一个客户端

    root@zlucelia:/mnt/f/wsl_go_workspace/p2p# go run client.go user2 localhost 9527 8092
    获得对象地址: {127.0.0.1 8091 }
    p2p>收到消息:打洞消息
    p2p>