libp2p算是一个蛮新的库,提供了非常强大的p2p节点发现/连接/通信能力。IPFS的基石。
中文网上的技术教程比较少,所以抛砖引玉开一点坑来体验一下!
这篇博客本质上就是用libp2p做一个NAT打洞建立p2p的实现,不过过程中能够学到很多libp2p的概念,当然用libp2p做也应该有不少好处有待进一步研究。
代码基于:
https://github.com/libp2p/go-libp2p/tree/master/examples
和一些外网教程
libp2p-node
, 或说Host
, 中文名节点, 不是指网络中的一个主机, 而是指的网络连接中的一个独立端点, 一个独立的应用程序, 一台电脑/服务器上面可以配置多个host
. 在go-libp2p
的example
中, 很多测试都是在同一个电脑上运行的.Multiaddr
, libp2p
网络传输层的协议中, 用一个Multiaddr
概念来统领两个节点之间的多种连接方式. 从而对更高层的应用隐藏底部的具体连接协议.两个藏在NAT或防火墙后面的host:A, B; 一个可以被公网访问到的(比如说具有公网IP)host:R
这里A是等待被访问的host, B是要去访问A的host
首先A需要先去连接中转服务器R, 进行一个预定Reservation
, 这样A和R的双向通路就打开了.
接下来B向R发出连接到A的请求, R帮助B与A建立连接
代码主要分为两个程序: 客户端和服务端/中转服务器
中转服务器
的核心代码: 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{}
只是用于卡住主进程别主线程退出让子线程也都退出了.
libp2p.NoListenAddrs
, 说明此host没有开启自己的监听端口, 任何人都没有办法直接向它发送连接请求.Reservation
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
的数据流
// 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()
}
}
$ go mod init xxxxx
$ go mod tidy
$ go run relayserver.go
这两串字符串是用来与中转服务器连接的门票.
先要把TCP协议的那一串中的本地ip改成公网ip,
然后ID填写在客户端代码的relayServerID
TCP协议串按照我的代码为例改填在下一行
$ go run main.go
这里我写好了一个提示了, 只要再开一个新的终端, 执行上述代码就可以打开Bhost并连接到A
$ go run .\main.go -d 12D3KooWNuA86qRCrvvXNnGrzGyY6bkEVWpPr4ffsCD9T5NUTyUX
后面的那一串就是A的ID
总的来说, 逻辑是非常清晰的, 其中有些概念在自己学习和实践过程中会慢慢掌握完整.
加油!