【go-libp2p学习笔记】使用go-libp2p搭建中转服务器(circuit relay server)

使用go-libp2p搭建中转服务器(circuit relay server)

libp2p算是一个蛮新的库,提供了非常强大的p2p节点发现/连接/通信能力。IPFS的基石。
中文网上的技术教程比较少,所以抛砖引玉开一点坑来体验一下!

这篇博客本质上就是用libp2p做一个NAT打洞建立p2p的实现,不过过程中能够学到很多libp2p的概念,当然用libp2p做也应该有不少好处有待进一步研究。

代码基于:
https://github.com/libp2p/go-libp2p/tree/master/examples
和一些外网教程

复现本篇博客的技术基础

  1. 基础的Go语言能力(我也不是很懂,GO能学的太多了,超过我上一篇博客的内容基本上都不会)
  2. Go version>=1.19(libp2p的要求)
  3. NAT与NAT打洞相关概念
  4. 一台具有公网IP的服务器
  5. 你自己的电脑

1. 了解一些libp2p基础概念

  1. libp2p-node, 或说Host, 中文名节点, 不是指网络中的一个主机, 而是指的网络连接中的一个独立端点, 一个独立的应用程序, 一台电脑/服务器上面可以配置多个host. 在go-libp2pexample中, 很多测试都是在同一个电脑上运行的.
  2. Multiaddr, libp2p网络传输层的协议中, 用一个Multiaddr概念来统领两个节点之间的多种连接方式. 从而对更高层的应用隐藏底部的具体连接协议.

2. 原理

两个藏在NAT或防火墙后面的host:A, B; 一个可以被公网访问到的(比如说具有公网IP)host:R
这里A是等待被访问的host, B是要去访问A的host
首先A需要先去连接中转服务器R, 进行一个预定Reservation, 这样A和R的双向通路就打开了.
接下来B向R发出连接到A的请求, R帮助B与A建立连接
【go-libp2p学习笔记】使用go-libp2p搭建中转服务器(circuit relay server)_第1张图片

3. 代码核心语句解释

代码主要分为两个程序: 客户端服务端/中转服务器

中转服务器的核心代码:
	relay1, err := libp2p.New()
 	_, err = relay.New(relay1)

    log.Printf("relay1Info ID: %v Addrs: %v",relay1.ID(), relay1.Addrs())

就这么短, 很简单.
第一句话创建一个新的host,
第二句话在host上配置中转服务器
第三句话将host的ID和地址打印出来, 这里我们手动复制传给A和B

客户端的核心代码:

客户端也分为两个部分, 等待被连接的Ahost, 和去连接A的Bhost.
这里我们直接用是否输入了被连接的Ahost的ID作为本客户端是A还是B的判断条件.
等待被连接的Ahost

	unreachableNode, err := libp2p.New(
		libp2p.NoListenAddrs,
		// Usually EnableRelay() is not required as it is enabled by default
		// but NoListenAddrs overrides this, so we're adding it in explictly again.
		libp2p.EnableRelay(),
	)


    relayServerID := "12D3KooWHwN52aYe18Wa6meTcLtrQ6y7jKXaQ85zf2snxsUWPA9B"
	ms1, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/43.143.xxx.xxx/tcp/38403/p2p/%v", relayServerID))
	relay1info, err := peer.AddrInfoFromP2pAddr(ms1)
    if err := unreachableNode.Connect(context.Background(), *relay1info); err != nil {
       log.Printf("Failed to connect unreachable1 and relay1: %v", err)
       return
	}
 	_, err = client.Reserve(context.Background(), unreachableNode, *relay1info)
    

	// Now, to test the communication, let's set up a protocol handler on unreachable2
	unreachableNode.SetStreamHandler("/chatStream", func(s network.Stream) {
		log.Println("Awesome! We're now communicating via the relay!")
		log.Println("Got a new stream!")

		// Create a buffer stream for non blocking read and write.
		rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))

		go readData(rw)
		go writeData(rw)
	})

    select{}

这里代码主要分为三个部分, 我分别用俩个空行进行分割, 最后的select{}只是用于卡住主进程别主线程退出让子线程也都退出了.

  1. 创建一个Ahost, 这里设定第一个参数libp2p.NoListenAddrs, 说明此host没有开启自己的监听端口, 任何人都没有办法直接向它发送连接请求.
  2. Ahost与中转服务器建立连接, 后面的实践会展示这里中转服务器的信息是如何给出的, 后进行Reservation
  3. Ahost注册自己的一个自定义连接协议, 这里做的是对打开的网络连接流进行读写协议. readData()``writeData()是两个自定义的数据流处理函数, 直接用的example的代码

2, 3两个部分可以互换位置.

连接A的Bhost

relayaddr, err := ma.NewMultiaddr("/p2p/" + relay1info.ID.String() + "/p2p-circuit/p2p/" + *dist)
useRelayToDistInfo, err := peer.AddrInfoFromP2pAddr(relayaddr)

s, err := unreachableNode.NewStream(network.WithUseTransient(context.Background(), "chatStream"), useRelayToDistInfo.ID, "/chatStream")
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
go writeData(rw)
go readData(rw)

第一句话是构造一个Multiaddr, 这个地址同时给出了中转服务器的地址, 和利用中转协议访问到A(dist中是A节点的id)的两个语义. go-libp2p的底层帮我们实现了这些语义的底层.
第二句话是将Multiaddr转化成一个PeerInfo,
用这个在第三句话及之后, 完成连接, 并打开指定协议chatStream的数据流

4. 完整代码

// relayserver.go
package main

import (
	"log"

	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/relay"


func main() {
	run()
}

func run() {
	relay1, err := libp2p.New()
	if err != nil {
		log.Printf("Failed to create relay1: %v", err)
		return
	}

	_, err = relay.New(relay1)
	if err != nil {
		log.Printf("Failed to instantiate the relay: %v", err)
		return
	}

	log.Printf("relay1Info ID: %v Addrs: %v",relay1.ID(), relay1.Addrs())

	
	select {}
}

//main.go

package main

import (
	"bufio"
	"context"
	"flag"
	"fmt"
	"log"
	"os"

	"github.com/libp2p/go-libp2p"
	"github.com/libp2p/go-libp2p/p2p/protocol/circuitv2/client"

	"github.com/libp2p/go-libp2p/core/network"
	"github.com/libp2p/go-libp2p/core/peer"

	ma "github.com/multiformats/go-multiaddr"
)

func main() {
	dist := flag.String("d", "", "your fanal nodeID")
	flag.Parse()
	run(dist)
}

func run(dist *string) {
	unreachableNode, err := libp2p.New(
		libp2p.NoListenAddrs,
		// Usually EnableRelay() is not required as it is enabled by default
		// but NoListenAddrs overrides this, so we're adding it in explictly again.
		libp2p.EnableRelay(),
	)
	if err != nil {
		log.Printf("Failed to create unreachable1: %v", err)
		return
	}

	relayServerID := "12D3KooWAxYf4KA2pEhwzoz4cs65biWREhqp6nTrdFTMNnY9vk1b"
	ms1, _ := ma.NewMultiaddr(fmt.Sprintf("/ip4/43.143.xxx.xxx/tcp/34301/p2p/%v", relayServerID))
	relay1info, err := peer.AddrInfoFromP2pAddr(ms1)

	if err := unreachableNode.Connect(context.Background(), *relay1info); err != nil {
		log.Printf("Failed to connect unreachable1 and relay1: %v", err)
		return
	}
	_, err = client.Reserve(context.Background(), unreachableNode, *relay1info)
	if err != nil {
		log.Printf("unreachable2 failed to receive a relay reservation from relay1. %v", err)
		return
	}

	unreachableNode.SetStreamHandler("/chatStream", func(s network.Stream) {
		log.Println("Awesome! We're now communicating via the relay!")
		log.Println("Got a new stream!")

		rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))

		go readData(rw)
		go writeData(rw)
	})

	if *dist == "" {
		//说明本节点是等待被另一个节点call的,只需要与中专节点连接一下注册到即可
		//写一个简单的阻塞
		fmt.Printf("WatingNode is onLine!")
		fmt.Printf("use \" go run .\\main.go -d %v \" in other CLI", unreachableNode.ID())
		select {}
	}

	relayaddr, err := ma.NewMultiaddr("/p2p/" + relay1info.ID.String() + "/p2p-circuit/p2p/" + *dist)
	if err != nil {
		log.Println(err)
		return
	}

	useRelayToDistInfo, err := peer.AddrInfoFromP2pAddr(relayaddr)
	if err := unreachableNode.Connect(context.Background(), *useRelayToDistInfo); err != nil {
		log.Printf("Unexpected error here. Failed to connect unreachable1 and unreachable2: %v", err)
		return
	}

	log.Println("Yep, that worked!")

	s, err := unreachableNode.NewStream(network.WithUseTransient(context.Background(), "chatStream"), useRelayToDistInfo.ID, "/chatStream")
	if err != nil {
		log.Println("Whoops, this should have worked...: ", err)
		return
	}

	rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))

	go writeData(rw)
	go readData(rw)

	select {}
}

func readData(rw *bufio.ReadWriter) {
	for {
		str, _ := rw.ReadString('\n')

		if str == "" {
			return
		}
		if str != "\n" {
			// Green console colour: 	\x1b[32m
			// Reset console colour: 	\x1b[0m
			fmt.Printf("\x1b[32m%s\x1b[0m> ", str)
		}

	}
}

func writeData(rw *bufio.ReadWriter) {
	stdReader := bufio.NewReader(os.Stdin)

	for {
		fmt.Print("> ")
		sendData, err := stdReader.ReadString('\n')
		if err != nil {
			log.Println(err)
			return
		}

		rw.WriteString(fmt.Sprintf("%s\n", sendData))
		rw.Flush()
	}
}

5. 实践

在公网服务器上执行relayserver.go
$ go mod init xxxxx
$ go mod tidy
$ go run relayserver.go 

结果如下:
20221101111205

我们要记录下ID,
20221101111722
和TCP协议的端口号
20221101111819

修改客户端代码, 以连接到中转服务器

这两串字符串是用来与中转服务器连接的门票.
先要把TCP协议的那一串中的本地ip改成公网ip,
然后ID填写在客户端代码的relayServerID
TCP协议串按照我的代码为例改填在下一行
20221101112259

执行本地的Ahost
$ go run main.go

20221101112345
这里我写好了一个提示了, 只要再开一个新的终端, 执行上述代码就可以打开Bhost并连接到A

执行本地的Bhost
$ go run .\main.go -d 12D3KooWNuA86qRCrvvXNnGrzGyY6bkEVWpPr4ffsCD9T5NUTyUX

后面的那一串就是A的ID

结果

完成p2p连接
20221101112602

6. 总结

总的来说, 逻辑是非常清晰的, 其中有些概念在自己学习和实践过程中会慢慢掌握完整.
加油!

你可能感兴趣的:(GO语言笔记,go,libp2p,p2p)