P2P网络编程-2-案例实践:P2P聊天应用

article/2025/9/27 22:12:41

文章目录

  • 一、初代版本
    • 1.1 简介
    • 1.2 代码与解析
    • 1.3 测试运行
  • 二、节点发现
    • 2.1 简介
    • 2.2 代码与解析
    • 2.3 测试运行
  • 三、总结
    • 3.1 libp2p节点发现构建流程
    • 3.2 libp2p中地址的转换关系

上一节学习了IPFS项目中P2P网络的构建工具libp2p的基本概念以及简单案例

接下来通过官方的聊天案例进一步了解该项目的使用

项目地址: https://github.com/libp2p/go-libp2p/tree/master/examples

我们从初代版本(手动发送接收)p2p聊天到具有节点发现功能的完全去中心化聊天学习libp2p的使用

一、初代版本

1.1 简介

项目使用Libp2p实现了一个简单的p2p聊天应用。该项目应用在两个节点中,需要至少满足下列条件一个:

  • 都有一个私有IP地址(在同一个网络下)
  • 至少有一个节点有一个公网地址

1.2 代码与解析

已将所有的注解写在代码中,可直接查看代码p2pChat.go

package mainimport ("bufio""context""crypto/rand""flag""fmt""github.com/libp2p/go-libp2p""github.com/libp2p/go-libp2p-core/crypto""github.com/libp2p/go-libp2p-core/host""github.com/libp2p/go-libp2p-core/network""github.com/libp2p/go-libp2p-core/peer""github.com/libp2p/go-libp2p-core/peerstore"mutiaddr "github.com/multiformats/go-multiaddr""io""log"mrand "math/rand""os"
)//
//  handleStream
//  @Description: 流处理函数
//  @param s	新的数据流
//
func handleStream(s network.Stream)  {log.Println("获得了新的流!")// 根据新的流创建读写流rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))// 创建两个携程分别读写数据go readData(rw)go writeData(rw)// 流s始终开启,直到流两端的任何一方关闭他
}//
//  readData
//  @Description: 按行读取字符串输出
//  @param rw
//
func readData(rw *bufio.ReadWriter)  {for {s, _ := rw.ReadString('\n')if s == "" {return}if s != "\n" {fmt.Printf("\x1b[32m%s\x1b[0m>", s)}}
}//
//  writeData
//  @Description: 获取标准输入,按行写入数据
//  @param rw
//
func writeData(rw *bufio.ReadWriter)  {// 1. 创建标准输入流stdReader := bufio.NewReader(os.Stdin)for {fmt.Printf(">")// 2. 读取标准输入s, err := stdReader.ReadString('\n')if err != nil {log.Panic(err)return}// 3. 使用读写流进行写入rw.WriteString(fmt.Sprintf("%s\n", s))rw.Flush()}
}//
//  makeHost
//  @Description: 生成一个节点主机
//  @param ctx	上下文环境
//  @param port	端口
//  @param randomness 随机源
//  @return host.Host
//  @return error
//
func makeHost(ctx context.Context, port int, randomness io.Reader) (host.Host, error) {// 1. 使用RSA和随机数流创建密钥对privKey, _, err := crypto.GenerateKeyPairWithReader(crypto.RSA, 2048, randomness)if err != nil {log.Panic(err)return nil, err}// 2. 根据端口构建多重地址newMultiaddr, err := mutiaddr.NewMultiaddr(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port))// 3. 创建节点return libp2p.New(ctx,libp2p.ListenAddrs(newMultiaddr),		// 设置地址libp2p.Identity(privKey),				// 设置密钥)
}//
//  startPeer
//  @Description: 节点被连接时开启流处理协议,仅被使用在流接受端
//  @param ctx
//  @param host
//  @param streamHandler
//
func startPeer(host host.Host, streamHandler network.StreamHandler)  {// 1. 设置流处理函数host.SetStreamHandler("/chat/1.0.0", streamHandler)// 2. 获取实际节点的TCP端口var port stringfor _, la := range host.Network().ListenAddresses() {// 获取当前节点Tcp协议监听的端口号// ValueForProtocol作用:获取mutiAddr中特殊协议的特殊值if tcpPort, err := la.ValueForProtocol(mutiaddr.P_TCP); err == nil {port = tcpPortbreak}}// 3. 输出if port == "" {log.Println("不能够找到真实本地端口")return}// host.ID().Pretty(): 返回节点ID的base58编码log.Printf("运行 './p2pChat -d /ip4/127.0.0.1/tcp/%v/p2p/%s' 在另一个控制台", port, host.ID().Pretty())log.Println("你可以用公网IP代替127.0.0.1")log.Println("等待连接中...")log.Println()
}//
//  startPeerAndConnect
//  @Description: 链接目标节点并创建流
//  @param ctx	上下文
//  @param host 主机节点
//  @param destination	目标节点字符串
//  @return *bufio.ReadWriter	缓冲读写流
//  @return error
//
func startPeerAndConnect(host host.Host, destination string) (*bufio.ReadWriter, error) {// 输出一下主机地址信息for _, la := range host.Addrs() {log.Printf(" - %v\n", la)}log.Println()// 1. 将目标节点地址转换为mutiaddr格式desMultiaddr, err := mutiaddr.NewMultiaddr(destination)if err != nil {log.Panic(err)return nil, err}// 2. 提取目标节点的Peer ID信息info, err := peer.AddrInfoFromP2pAddr(desMultiaddr)if err != nil {log.Panic(err)return nil, err}// 3. 将目标节点的信息(id和地址)加入当前主机节点的节点池(后面的创建链接、流都需要使用)host.Peerstore().AddAddrs(info.ID, info.Addrs, peerstore.PermanentAddrTTL)// 4. 向目标节点开启流// context.Background(): 是一个空环境,一般用于初始化、测试newStream, err := host.NewStream(context.Background(), info.ID, "/chat/1.0.0")if err != nil {log.Panic(err)return nil, err}log.Println("已建立目标节点的连接")// 5. 根据新的流创建缓冲读写流返回rw := bufio.NewReadWriter(bufio.NewReader(newStream), bufio.NewWriter(newStream))return rw, nil
}func main()  {// 1. 创建程序上下文环境ctx, cancel := context.WithCancel(context.Background())defer cancel()// 2. 命令行程序逻辑sourcePort := flag.Int("sp", 0, "源端口号")dest := flag.String("d", "", "目标地址字符串")help := flag.Bool("h", false, "帮助")debug := flag.Bool("debug", false, "Debug模式:每次执行都生成相同的node ID")flag.Parse()	// 解析输入命令行// 帮助if *help {fmt.Printf("这是一个使用libp2p编写的简单P2P聊天程序\n\n")fmt.Println("Usage: Run './p2pChat -sp <SOURCE_PORT>' where <SOURCE_PORT> can be any port number.")fmt.Println("Now run './p2pChat -d <MULTIADDR>' where <MULTIADDR> is multiaddress of previous listener host.")os.Exit(0)}// debug模式var r io.Readerif *debug {// 创建公私钥的时候使用相同的随机源(sourcePort)使得每次生成相同的Peer ID,不要在正式环境使用// 注意:mrand是math/rand,下面的rand是crypto/randr = mrand.New(mrand.NewSource(int64(*sourcePort)))}else {r = rand.Reader}// 创建主机h, err := makeHost(ctx, *sourcePort, r)if err != nil {log.Panic(err)return}// 开启主机if *dest == "" {// 接收连接方startPeer(h, handleStream) 		// 将自己写的流处理函数导入}else {// 发送连接方rw, err := startPeerAndConnect(h, *dest)if err != nil {log.Panic(err)return}// 创建携程读写go readData(rw)go writeData(rw)}// 一直阻塞select {}
}

1.3 测试运行

我的环境:

  • 一个公网IP服务器:47.103.203.133
  • 一个本地电脑

首先启动连接接受者(公网服务器)端口1234:

go build p2pChat.go 
./p2pChat -sp 1234

WHeG9e

然后启动连接发起者本地电脑,注意:将127.0.0.1换成服务器IP地址:

./p2pChat -d /ip4/47.103.203.133/tcp/1234/p2p/QmSz6C8oMoNEAUJyQkBFbPXLZTGgRpmT85npqtYGAr9Vyb

ggkAZZ

如此,两者就可以通话了~!

4l6pgZ

二、节点发现

2.1 简介

上面的方法需要提前了解节点的地址才能够连接上,真正的p2p需要实现节点的自动发现

以下两个步骤在上面的初代版本中已经实现:

  1. 创建上下文环境
  2. 配置一个p2p host节点
  3. 设置本地流默认处理函数

现在节点发现需要实现以下步骤:

  1. 初始化一个新的 D H T DHT DHT客户端与host主机对等
  2. 连接IPFS的引导节点(使用DHT发现附近的引导节点)
  3. 使用相同的键申明同一p2p节点网络(在聚会前约好地点)
  4. 寻找附近的对等点
  5. 向附近对等点开启stream

2.2 代码与解析

p2pChat.go文件和详解如下:

package mainimport ("bufio""context""flag""fmt""github.com/ipfs/go-log/v2""github.com/libp2p/go-libp2p""github.com/libp2p/go-libp2p-core/network""github.com/libp2p/go-libp2p-core/peer""github.com/libp2p/go-libp2p-core/protocol"discovery "github.com/libp2p/go-libp2p-discovery"dht "github.com/libp2p/go-libp2p-kad-dht""github.com/multiformats/go-multiaddr""os""sync"
)// 重制一个事件名称
var logger = log.Logger("rendezvous")//
//  handleStream
//  @Description: 流处理函数
//  @param s	新的数据流
//
func handleStream(s network.Stream)  {logger.Info("获得了新的流!")// 根据新的流创建读写流rw := bufio.NewReadWriter(bufio.NewReader(s), bufio.NewWriter(s))// 创建两个携程分别读写数据go readData(rw)go writeData(rw)// 流s始终开启,直到流两端的任何一方关闭他
}//
//  readData
//  @Description: 按行读取字符串输出
//  @param rw
//
func readData(rw *bufio.ReadWriter)  {for {s, _ := rw.ReadString('\n')if s == "" {return}if s != "\n" {fmt.Printf("\x1b[32m%s\x1b[0m>", s)}}
}//
//  writeData
//  @Description: 获取标准输入,按行写入数据
//  @param rw
//
func writeData(rw *bufio.ReadWriter)  {// 1. 创建标准输入流stdReader := bufio.NewReader(os.Stdin)for {fmt.Printf(">")// 2. 读取标准输入s, err := stdReader.ReadString('\n')if err != nil {panic(err)}// 3. 使用读写流进行写入rw.WriteString(fmt.Sprintf("%s\n", s))rw.Flush()}
}func main()  {// 1.设置log日志输出等级log.SetAllLoggers(log.LevelWarn)log.SetLogLevel("rendezvous", "info")// 2. 命令行help := flag.Bool("h", false, "帮助")// 解析config, err := ParseFlags()if err != nil {panic(err)}// 帮助if *help {fmt.Println("This program demonstrates a simple p2p chat application using libp2p")fmt.Println()fmt.Println("Usage: Run './p2pChat in two different terminals. Let them connect to the bootstrap nodes, announce themselves and connect to the peers")flag.PrintDefaults()return}// 3. 创建上下文ctx, cancel := context.WithCancel(context.Background())defer cancel()// 4. 创建本地节点host, err := libp2p.New(ctx,libp2p.ListenAddrs([]multiaddr.Multiaddr(config.ListenAddresses)...), // 如果配置了监听节点就初始化加入)if err != nil {panic(err)}logger.Info("创建本地节点: ", host.ID())logger.Info(host.Addrs())// 5. 开启本地节点流处理函数host.SetStreamHandler(protocol.ID(config.ProtocolID), handleStream)// 6. 初始化DHT客户端kademliaDHT, err := dht.New(ctx, host)if err != nil {panic(err)}// 7. 引导DHT客户端,让本地节点维护自己的DHT,这样就可以在未来没有引导节点时加入新的节点logger.Debug("引导DHT中...")if err := kademliaDHT.Bootstrap(ctx); err != nil {panic(err)}// 8. 连接引导节点var wg sync.WaitGroup // 创建多携程等待池for _, peerAddr := range config.BootstrapPeers {// 转换地址格式: mutiaddr -> AddrInfopeerInfo, _ := peer.AddrInfoFromP2pAddr(peerAddr)wg.Add(1)		// 加入携程go func() {defer wg.Done()	// 预先关闭携程// 连接任务if err := host.Connect(ctx, *peerInfo); err != nil {logger.Warn(err)}else {logger.Info("成功连接引导节点: ", peerInfo.ID)}}()}wg.Wait()			// 阻塞等待所有携程结束// 9. 使用相同的键申明同一p2p节点网络(在聚会前约好地点)logger.Info("正在声明当前网络...")// RoutingDiscovery是内容路由的一个实例routingDiscovery := discovery.NewRoutingDiscovery(kademliaDHT)// 持续的申明一个服务并用一个键唯一标记discovery.Advertise(ctx, routingDiscovery, config.RendezvousString)logger.Info("成功申明网络!唯一标识符:", config.RendezvousString)// 10. 在申明的网络中寻找其他节点logger.Info("正在寻找其他节点")peerChan, err := routingDiscovery.FindPeers(ctx, config.RendezvousString)	// 返回的是一个AddrInfo Channelif err != nil {panic(err)}// 遍历for peer := range peerChan {if peer.ID == host.ID() {continue}logger.Debug("找到新节点: ", peer.ID)logger.Debug("开始连接新节点: ", peer.ID)stream, err := host.NewStream(ctx, peer.ID, protocol.ID(config.ProtocolID))if err != nil {logger.Warn("连接节点 ", peer.ID, " 失败!")logger.Warn(err)continue}else {// 成功连接,创建读写流rw := bufio.NewReadWriter(bufio.NewReader(stream), bufio.NewWriter(stream))go writeData(rw)go readData(rw)}logger.Info("成功连接节点: ", peer.ID)}select {}
}

flag.go文件如下:

package mainimport ("flag"dht "github.com/libp2p/go-libp2p-kad-dht"maddr "github.com/multiformats/go-multiaddr""strings"
)// 重命名多地址数组类型
type addrList []maddr.Multiaddr// 将flag命令输入数据存储在自定义结构体中需要实现Value接口,需要实现两个抽象函数String和Set
func (al *addrList) String() string {strs := make([]string, len(*al))for i, addr := range *al {strs[i] = addr.String()}return strings.Join(strs, ",")
}func (al *addrList) Set(value string) error {addr, err := maddr.NewMultiaddr(value)if err != nil {return err}*al = append(*al, addr)return nil
}func StringsToAddrs(addrStrings []string) (maddrs []maddr.Multiaddr, err error) {for _, addrString := range addrStrings {addr, err := maddr.NewMultiaddr(addrString)if err != nil {return maddrs, err}maddrs = append(maddrs, addr)}return
}// 本地节点配置环境(代替数据库)
type Config struct {RendezvousString stringBootstrapPeers   addrList		// 引导节点集合ListenAddresses  addrList		// 监听节点集合ProtocolID       string
}func ParseFlags() (Config, error){config := Config{}flag.StringVar(&config.RendezvousString, "rendezvous", "19021902","标识一组节点的唯一字符串。与你的朋友分享这些,让他们与你联系")flag.Var(&config.BootstrapPeers, "peer", "向当前节点添加一组引导节点数组(mutiaddress格式)")flag.Var(&config.ListenAddresses, "listen", "向当前节点添加一组监听节点数组(mutiaddress格式)")flag.StringVar(&config.ProtocolID, "pid", "/p2pChat/1.1.0", "给stram Hander设置一个协议号")flag.Parse()if len(config.BootstrapPeers) == 0 {// 如果没有设置引导节点,那么就使用dht默认的引导节点集合config.BootstrapPeers = dht.DefaultBootstrapPeers}return config, nil
}

2.3 测试运行

go build -o p2pChat ./
./p2pChat -listen /ip4/0.0.0.0/tcp/8888
# 另一台机器
./p2pChat -listen /ip4/0.0.0.0/tcp/6666

T2JS2T

uRcN3n

三、总结

3.1 libp2p节点发现构建流程

libp2p_host

3.2 libp2p中地址的转换关系

libp2p_addr

学习资料来源:

https://github.com/libp2p/go-libp2p/tree/master/examples/chat

https://github.com/libp2p/go-libp2p/tree/master/examples/chat-with-rendezvous

觉得不错的话,请点赞关注呦~~你的关注就是博主的动力
关注公众号,查看更多go开发、密码学和区块链科研内容:
2DrbmZ


http://chatgpt.dhexx.cn/article/869yo3hd.shtml

相关文章

p2p网络实现(C++)

p2p网络&#xff08;对等网络&#xff09;&#xff1a;对等网络是一种网络结构的思想。它与目前网络中占据主导地位的客户端/服务器结构的一个本质区别是&#xff0c;整个网络结构中不存在中心节点。在P2P结构中&#xff0c;每一个节点&#xff08;peer&#xff09;大都同时具有…

区块链中Java基于WebSocket构建P2P网络

一、pom依赖 <dependency><groupId>org.java-websocket</groupId><artifactId>Java-WebSocket</artifactId><version>1.5.1</version></dependency>二、服务端代码 package com.peck.blockchain.p2p;import org.java_websock…

区块链P2P网络

区块链P2P网络 阅读大概需要10分钟 前言 上两篇文章中我们聊了共识机制&#xff0c;今天我们聊一下区块链技术中的另外一个核心技术点&#xff1a;P2P网络&#xff08;Peer to peer networking&#xff09;。首先澄清一点的是这里讲的P2P这个概念跟平时我们在互联网金融圈提及…

Rust P2P网络应用实战-1 P2P网络核心概念及Ping程序

本系列文章首先研究P2P网络的核心概念&#xff0c;然后详细分析libp2p-rust库中的应用实例&#xff0c;为以后开发P2P网络应用程序打好基础。 P2P网络 P2P(Peer-to-Peer)是一种网络技术&#xff0c;可以在网络中不同的计算机上共享各种计算资源&#xff0c;如CPU、网络带宽和存…

区块链P2P网络协议演进过程

区块链是以加密机制、储存机制、共识机制等多种技术组成的分布式系统&#xff0c;可以在无中心服务器的情况下实现相互信任的点对点交易功能。区块链最大的特点是去中心化和分布式&#xff0c;区块链共识机制使得参与节点共同为系统提供服务&#xff0c;实现中心化系统中类似金…

【区块链实战】什么是 P2P 网络,区块链和 P2P 网络有什么关系

目录 一、简介 二、知识点 P2P 网络 区块链节点与 P2P 的关系 区块链节点功能分类 P2P 网络特征 三、什么是 P2P 网络&#xff0c;区块链式使用 P2P 网络做什么 1、P2P 网络概念 2、P2P 网络节点特征 3、P2P 与区块链 4、网络节点功能 一、简介 在白皮书中&#xf…

Peer to Peer ( P2P ) 综述

Peer to Peer ( P2P ) 综述 罗杰文 luojwics.ict.ac.cn 中科院计算技术研究所 2005-11-3 1 绪言 1.1 Peer-To-Peer 介绍 最近几年&#xff0c;对等计算 目前,在学术界、工业界对于P2P没有一个统一的定义&#xff0c;下面列举几个常用的定义供参考&#xff1a; 定义:1、Pe…

Ubuntu对分区扩容

Ubuntu对分区扩容 准备工具 1、U盘 2、准备好的内存空间 3、Ubuntu的镜像文件 4、清楚如何进入自己电脑的BIOS 扩容大致方向try Ubuntu 网络上的扩容方法大都是在自己的Ubuntu下使用GParted,对将要扩容的分区进行卸载&#xff0c;然后进行扩容&#xff0c;但是这样做有个前…

linux ubuntu 分区,查看Ubuntu分区列表方法

今天在Ubuntu修复Grub正好碰到了要展示分区列表看看 不然都不清楚哪个是哪个了。。 sudo fdisk -l 就是这个命令 ubuntuubuntu:~$ sudo fdisk -l Disk /dev/sda: 250.0 GB, 250059350016 bytes 255 heads, 63 sectors/track, 30401 cylinders Units cylinders of 16065 * 512 …

U盘安装ubuntu20.04 Linux系统分区方案 Invalid Partition Table

一、简介&#xff1a; 一般磁盘分区表有两种格式&#xff1a;MBR和GPT&#xff0c;目前主要的BIOS系统引导方式也有两种&#xff1a;传统的Legacy BIOS和新型的UEFI BIOS 如果主机BIOS系统引导方式采用传统的Legacy BIOS&#xff0c;那么安装ubuntu系统的磁盘分区表使用MBR格式…

Android布局

目录 1. Android的基础布局 2. LinearLayout 线性布局 3. RelativeLayout 4. 常用的控件 1. Android的基础布局 LinearLayout 线性布局 RelativeLayout 相对布局 TableLayout 表格布局 FrameLayout 帧布局&#xff08;框架布局&#xff09; ConstrantLayout 约束布局 &…

安卓布局简单归纳

安卓布局 1.线性布局LinearLayout 2.表格布局TableLayout以及网格布局GridLayout 3.帧布局FrameLayout 4.相对布局RelativeLayout 5.Android2.0已经过时的绝对布局AbsoluteLayout 这里仅谈xml的实现、不涉及java实现 布局管理器及组件的常用共有属性&#xff1a; 属性作用and…

安卓layout布局三等分

关于android LinearLayout的比例布局&#xff0c;主要有以下三个属性需要设置&#xff1a; 1&#xff0c;android:layout_width&#xff0c;android:layout_height,android:layout_weight三个值 2&#xff0c;当为水平布局时&#xff0c;android:layout_height“0dp",当为…

安卓APP(3)——安卓布局控件

嵌入式之路&#xff0c;贵在日常点滴 ---阿杰在线送代码 目录 一、布局的种类 二、布局和页面的关系 三、显示一张美女图 控件的宽度和高度 四、布局背景颜色&#xff0c;背景图&#xff0c;显示两个美女 关于控件ID 五、常用布局之相对布局 RelativeLayout中子控件常…

安卓的相对布局与线性布局

一、安卓布局的种类 Android共有七大基本布局。 分别是&#xff1a;线性布局LinearLayout、表格布局TableLayout、相对布局RelativeLayout、帧布局FrameLayout、绝对布局AbsoluteLayout、网格布局GridLayout。约束布局ConstraintLayout。 其中&#xff0c;表格布局是线性布局的…

Android:布局

Android&#xff1a;布局 LinearLayoutRelativeLayoutFrameLayoutTableLayoutGridLayoutConstraintLayout LinearLayout <LinearLayout xmlns:android"http://schemas.android.com/apk/res/android"android:layout_height"match_parent"android:layout_…

安卓六大布局之 线性布局(LinearLayout)

Android的界面是有布局和组件协同完成的&#xff0c;布局好比是建筑里的框架&#xff0c;而组件则相当于建筑里的砖瓦。组件按照布局的要求依次排列&#xff0c;就组成了用户所看见的界面。 Android的六大布局分别是 LinearLayout&#xff08;线性布局&#xff09;RelativeLayo…

Android-布局管理器

线性布局(Linearlayout) 属性 orientation 布局管理器内组件的排列方式(horizontal&#xff08;水平&#xff09;和vertical&#xff08;垂直&#xff09;&#xff0c;默认值为 horizontal.) layout_weight 权重 用于设置组件占父容器剩余空间的比例 la…

android 布局

android学习笔记&#xff08;一 android布局学习&#xff09; 转自http://blog.sina.com.cn/s/blog_61c62a960100ev3q.html (2009-09-20 20:50:44) 转载 标签&#xff1a; it 分类&#xff1a;android 最近痴迷上了android &#xff0c; 因为有java 语言的基础学起来自己感觉很…