1.案例下载
https://github.com/libp2p/go-libp2p/tree/master/examples
2.chat案例
这段代码是一个简单的基于libp2p的P2P聊天应用程序的示例。它允许两个节点通过P2P连接进行聊天。前提是:
- 两者都有私有IP地址(同一网络)。
- 至少其中一个具有公共IP地址。
假设如果’A’和’B’在不同的网络上,主机’A’可能有或可能没有公共IP地址,但主机’B’一定有一个公共IP地址。
//在一个命令行输入
`./chat -sp 3001`
//在另一个命令后输入一下代码,<MULTIADDR_B>`是前一个与前节点通信的标识
`./chat -d <MULTIADDR_B>`
运行后效果如下:
3.源码分析
3.1 main函数
func main() {
// 创建一个上下文和取消函数以进行 graceful shutdown
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
// 定义命令行参数
sourcePort := flag.Int("sp", 0, "Source port number")
dest := flag.String("d", "", "Destination multiaddr string")
help := flag.Bool("help", false, "Display help")
debug := flag.Bool("debug", false, "Debug generates the same node ID on every execution")
// 解析命令行参数
flag.Parse()
// 如果传递了-help参数,显示帮助信息并退出
if *help {
fmt.Printf("This program demonstrates a simple p2p chat application using libp2p\n\n")
fmt.Println("Usage: Run './chat -sp <SOURCE_PORT>' where <SOURCE_PORT> can be any port number.")
fmt.Println("Now run './chat -d <MULTIADDR>' where <MULTIADDR> is multiaddress of previous listener host.")
os.Exit(0)
}
// 如果启用调试模式,则使用常量随机源生成对等方ID。仅用于调试,默认情况下关闭。否则,它将使用 rand.Reader。
var r io.Reader
if *debug {
// 使用端口号作为随机源。
// 如果使用相同的端口号,这将在多次执行中始终生成相同的主机ID。
// 在生产代码中永远不要这样做。
r = mrand.New(mrand.NewSource(int64(*sourcePort)))
} else {
r = rand.Reader
}
// 创建libp2p主机
h, err := makeHost(*sourcePort, r)
if err != nil {
log.Println(err)
return
}
// 如果未指定目标地址,则作为监听者启动节点
if *dest == "" {
startPeer(ctx, h, handleStream)
} else {
// 如果指定了目标地址,表明这是一个主动连接的节点。需要创建线程以读取和写入数据
rw, err := startPeerAndConnect(ctx, h, *dest)
if err != nil {
log.Println(err)
return
}
// 创建线程以读取和写入数据
go writeData(rw)
go readData(rw)
}
// 永久等待
select {}
}
3.2 makeHost
// makeHost函数用于创建libp2p主机
func makeHost(port int, randomness io.Reader) (host.Host, error) {
// 为此主机创建一个新的RSA密钥对,返回私钥、公钥、错误信息
prvKey, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, randomness)
if err != nil {
log.Println(err)
return nil, err
}
// 0.0.0.0 将监听任何接口设备。
sourceMultiAddr, _ := multiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))
// libp2p.New 用于构建一个新的libp2p主机。
// 这里可以添加其他选项。
return libp2p.New(
libp2p.ListenAddrs(sourceMultiAddr), // 设置主机监听的地址
libp2p.Identity(prvKey), // 设置主机的身份,即密钥对
)
}
3.3 startPeer
// startPeer函数用于启动作为监听者的节点
func startPeer(ctx context.Context, h host.Host, streamHandler network.StreamHandler) {
// 设置一个函数作为流处理器。
// 当节点连接时,此函数被调用,并启动一个带有该协议的流。
// 仅适用于接收方,这里使用的streamHandler是代码中的handleStream
h.SetStreamHandler("/chat/1.0.0", streamHandler)
// 让我们从我们的监听多地址中获取实际的TCP端口,以防我们使用0(默认值;随机可用端口)。
var port string
for _, la := range h.Network().ListenAddresses() {
if p, err := la.ValueForProtocol(multiaddr.P_TCP); err == nil {
port = p
break
}
}
if port == "" {
log.Println("无法找到实际的本地端口")
return
}
log.Printf("在另一个控制台中运行 './chat -d /ip4/127.0.0.1/tcp/%v/p2p/%s'\n", port, h.ID())
log.Println("您也可以用公共IP替换 127.0.0.1。")
log.Println("等待传入连接")
log.Println()
}
3.4 startPeerAndConnect
// startPeerAndConnect函数用于启动作为主动连接者的节点并连接到目标地址
func startPeerAndConnect(ctx context.Context, h host.Host, destination string) (*bufio.ReadWriter, error) {
log.Println("此节点的多地址:")
for _, la := range h.Addrs() {
log.Printf(" - %v\n", la)
}
log.Println()
// 将目标地址转换为multiaddr。
maddr, err := multiaddr.NewMultiaddr(destination)
if err != nil {
log.Println(err)
return nil, err
}
// 从multiaddr中提取对等方ID。
info, err := peer.AddrInfoFromP2pAddr(maddr)
if err != nil {
log.Println(err)
return nil, err
}
// 重要:在peerstore中添加目标对等方的对等多地址。
// 这将在libp2p的连接和流创建过程中使用。
// info.ID是一个peer的唯一标识,根据ID可以找到对应的多地址
h.Peerstore().AddAddrs(info.ID, info.Addrs, peerstore.PermanentAddrTTL)
// 与目标建立流。
// 使用 'peerId' 从peerstore中获取目标对等方的多地址。
s, err := h.NewStream(context.Background(), info.ID, "/chat/1.0.0")
if err != nil {
log.Println(err)
return nil, err
}
log.Println("已建立到目标的连接")
// 创建一个带有缓冲的流,以使读取和写入操作不会阻塞。
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
return rw, nil
}
3.5 handleStream
func handleStream(s network.Stream) {
log.Println("Got a new stream!")
// 创建一个不堵塞的读写缓冲流
rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))
go readData(rw)
go writeData(rw)
// 流 's' 将保持打开状态,直到您关闭它(或另一侧关闭它)。
}
3.6 readData和writeData
// readData函数用于从读写器中读取数据并在控制台上显示
func readData(rw *bufio.ReadWriter) {
for {
// 从读写器中读取字符串,直到遇到换行符 '\n'
str, _ := rw.ReadString('\n')
// 如果字符串为空,则退出循环
if str == "" {
return
}
// 如果字符串不是空行 '\n'
if str != "\n" {
// 控制台显示绿色文本: \x1b[32m
// 重置控制台文本颜色: \x1b[0m
fmt.Printf("\x1b[32m%s\x1b[0m> ", str)
}
}
}
// writeData函数用于从标准输入读取数据并将其写入到读写器中
func writeData(rw *bufio.ReadWriter) {
// 创建一个用于读取标准输入的读取器
stdReader := bufio.NewReader(os.Stdin)
for {
// 提示符
fmt.Print("> ")
// 从标准输入读取数据,直到遇到换行符 '\n'
sendData, err := stdReader.ReadString('\n')
if err != nil {
log.Println(err)
return
}
// 将数据写入读写器,并刷新缓冲
rw.WriteString(fmt.Sprintf("%s\n", sendData))
rw.Flush()
}
}