第 3 部分:集成 Identify 协议

0 阅读13分钟

本部分在 mDNS 节点发现的基础上,添加了 Identify 协议。Identify 协议是 libp2p 中的一个核心协议,允许节点在建立连接时自动交换身份信息,包括协议版本、支持的协议、节点公钥等。通过本示例,你将学习如何集成多个 NetworkBehaviour,构建功能更丰富的 P2P 节点,以及如何处理来自不同行为的事件。Identify 协议对于构建可互操作的 P2P 网络至关重要,它使得节点能够了解彼此的能力和特性,为后续的通信和协作奠定基础。

核心功能和目标

  • 实现 Identify 协议功能
  • 集成多个 NetworkBehaviour
  • 处理来自不同行为的事件
  • 交换节点身份信息

在整个项目中的位置和作用

  • 作为第三个实践示例
  • 展示如何集成多个 NetworkBehaviour
  • 为后续功能添加身份信息交换能力
  • 实现节点能力和特性的自动发现

学习该部分的预期收益

  • 掌握 Identify 协议的工作原理
  • 学会集成多个 NetworkBehaviour
  • 理解如何处理来自不同行为的事件
  • 为构建可互操作的 P2P 网络奠定基础

3.1 创建 Identify 项目

crates 目录下创建一个新的子项目:

cd crates
cargo new identify

3.2 修改 crates/identify/Cargo.toml 文件

配置 Identify 项目的依赖项:

[package]
name = "identify"
version.workspace = true
authors.workspace = true
license.workspace = true
edition.workspace = true
publish.workspace = true

[dependencies]
tracing.workspace = true
tracing-subscriber.workspace = true
tokio.workspace = true
anyhow.workspace = true

libp2p = { workspace = true, features = [
  "tcp",        # TCP 传输
  "noise",      # 噪声协议加密
  "yamux",      # 多路复用
  "mdns",       # mDNS 节点发现
  "tokio",      # Tokio 运行时支持
  "ping",       # Ping 协议
  "macros",     # 网络行为宏
  "identify",   # Identify 协议
] }

依赖说明

  • identify:提供 Identify 协议实现,用于节点身份信息交换
  • macros:提供网络行为宏,用于组合多个行为

3.3 修改 crates/identify/src/main.rs 文件

实现带有 Identify 协议的 P2P 节点:

use std::time::Duration;

use libp2p::{
  PeerId, Swarm, Transport, core::upgrade, futures::StreamExt, identify, identity, mdns, noise,
  ping, swarm, tcp, yamux,
};

#[derive(swarm::NetworkBehaviour)]
#[behaviour(to_swarm = "MyBehaviourEvent")]
struct MyBehavior {
  ping: ping::Behaviour,
  mdns: mdns::tokio::Behaviour,
  identify: identify::Behaviour,
}

// 2. 定义事件枚举
#[derive(Debug)]
enum MyBehaviourEvent {
  Ping(ping::Event),
  Mdns(mdns::Event),
  Identify(identify::Event),
}

impl From<ping::Event> for MyBehaviourEvent {
  fn from(value: ping::Event) -> Self {
    MyBehaviourEvent::Ping(value)
  }
}
impl From<mdns::Event> for MyBehaviourEvent {
  fn from(value: mdns::Event) -> Self {
    MyBehaviourEvent::Mdns(value)
  }
}
impl From<identify::Event> for MyBehaviourEvent {
  fn from(value: identify::Event) -> Self {
    MyBehaviourEvent::Identify(value)
  }
}

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  // 初始化日志追踪器,启用ANSI颜色输出
  tracing_subscriber::fmt().with_ansi(true).init();
  // 输出启动信息
  tracing::info!("start......");

  let key = identity::Keypair::generate_ed25519();
  let peer_id = PeerId::from(key.public());
  tracing::info!("Local peer id: {:?}", peer_id);

  // 创建传输层
  let transport = tcp::tokio::Transport::new(tcp::Config::default())
    .upgrade(upgrade::Version::V1)
    .authenticate(noise::Config::new(&key)?)
    .multiplex(yamux::Config::default())
    .timeout(Duration::from_secs(60))
    .boxed();

  // 创建 mDNS
  let mdns = mdns::tokio::Behaviour::new(mdns::Config::default(), peer_id)?;

  let ping = ping::Behaviour::new(
    ping::Config::new()
      .with_interval(Duration::from_secs(15))
      .with_timeout(Duration::from_secs(60)),
  );

  let identify = identify::Behaviour::new(identify::Config::new(
    "demo/1.0.0".to_string(),
    key.public(),
  ));

  let behaviour = MyBehavior {
    ping,
    mdns,
    identify,
  };

  let mut swarm = Swarm::new(
    transport,
    behaviour,
    peer_id,
    swarm::Config::with_tokio_executor().with_idle_connection_timeout(Duration::from_secs(60)),
  );

  swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;

  tracing::info!("🚀 Identify 节点启动,开始交换节点信息...");

  loop {
    match swarm.select_next_some().await {
      swarm::SwarmEvent::NewListenAddr { address, .. } => tracing::info!("👂 监听地址: {address}"),
      swarm::SwarmEvent::Behaviour(event) => match event {
        MyBehaviourEvent::Ping(event) => match event {
          ping::Event { peer, result, .. } => match result {
            Ok(rtt) => tracing::info!("🏓 Ping成功: {} RTT: {:?}", peer, rtt),
            Err(_) => tracing::error!("💥 Ping失败: {}", peer),
          },
        },
        MyBehaviourEvent::Mdns(event) => match event {
          mdns::Event::Discovered(list) => {
            let local_peer_id = *swarm.local_peer_id();
            for (peer_id, multiaddr) in list {
              // 避免连接到自己
              if peer_id != local_peer_id {
                tracing::info!("🔍 发现新节点: {}", peer_id);
                tracing::info!("🔗 节点地址: {}", multiaddr);
                // 尝试连接,忽略连接错误
                match swarm.dial(multiaddr) {
                  Ok(_) => tracing::info!("🔗 连接请求已发送: {}", peer_id),
                  Err(e) => tracing::error!("❌ 连接错误: {:?}", e),
                }
              }
            }
          }
          mdns::Event::Expired(list) => {
            for (peer_id, _multiaddr) in list {
              tracing::warn!("👋 节点离线: {}", peer_id);
            }
          }
        },
        MyBehaviourEvent::Identify(event) => match event {
          identify::Event::Received { peer_id, info, .. } => {
            tracing::info!("📣 节点信息: {peer_id} {info:?}");
          }
          identify::Event::Sent { peer_id, .. } => {
            tracing::info!("📣 发送节点信息: {peer_id}");
          }
          identify::Event::Error { peer_id, error, .. } => {
            tracing::error!("❌ 节点信息错误: {peer_id} {error:?}");
          }
          identify::Event::Pushed { peer_id, info, .. } => {
            tracing::info!("📣 节点信息已推送: {peer_id} {info:?}");
          }
        },
      },
      swarm::SwarmEvent::ConnectionEstablished { peer_id, .. } => {
        tracing::info!("✅ 连接成功: {peer_id}")
      }
      swarm::SwarmEvent::ConnectionClosed { peer_id, .. } => {
        tracing::error!("❌ 连接关闭: {peer_id}")
      }
      swarm::SwarmEvent::IncomingConnection { .. } => tracing::info!("🔗 接受连接"),
      swarm::SwarmEvent::IncomingConnectionError { error, .. } => {
        tracing::error!("❌ 接受连接错误: {error:?}")
      }
      swarm::SwarmEvent::OutgoingConnectionError { peer_id, error, .. } => {
        tracing::error!("❌ 连接 {peer_id:?} 错误: {error:?}")
      }
      swarm::SwarmEvent::Dialing { peer_id, .. } => tracing::info!("🔗 正在连接: {peer_id:?}"),
      _ => {}
    }
  }
}

代码详细解析:

  1. 扩展网络行为

    • MyBehavior 结构体中添加了 identify::Behaviour,实现节点身份信息交换
    • 使用 #[derive(swarm::NetworkBehaviour)] 宏自动实现 NetworkBehaviour trait
    • 组合了 ping::Behaviourmdns::tokio::Behaviouridentify::Behaviour
  2. 扩展事件枚举

    • MyBehaviourEvent 中添加了 Identify 变体,用于处理 Identify 事件
    • 现在事件枚举包含 PingMdnsIdentify 三个变体
  3. 事件转换

    • 实现 From<identify::Event> for MyBehaviourEvent trait,将 Identify 事件转换为自定义事件
    • 保持之前的 From<ping::Event>From<mdns::Event> 实现
    • 这样可以统一处理来自不同行为的事件
  4. Identify 初始化

    • 创建 identify::Behaviour 实例,配置协议版本和节点公钥
    • 协议版本设置为 "demo/1.0.0",用于标识应用程序类型和版本
    • 传入节点的公钥,用于身份验证和识别
  5. 传输层配置

    • 与 mDNS 示例类似,但添加了 60 秒的超时设置
    • 这有助于处理网络不稳定的情况,特别是在节点信息交换过程中
  6. 事件处理

    • Identify 事件
      • Received:收到其他节点的身份信息,包含协议版本、支持的协议、节点公钥等
      • Sent:成功发送本节点的身份信息给其他节点
      • Error:身份信息交换出错,可能是网络问题或节点配置问题
      • Pushed:主动推送身份信息给其他节点,通常在节点信息发生变化时
    • 其他事件:保持与之前示例相同的处理逻辑,包括 Ping 事件、mDNS 事件和连接事件

3.4 Identify 协议工作原理

Identify 协议是 libp2p 中的一个核心协议,其工作原理如下:

  1. 连接建立:当两个节点建立连接后,Identify 协议自动启动
  2. 信息交换:节点相互发送自己的身份信息,包括:
    • 协议版本
    • 支持的协议列表
    • 节点公钥
    • 节点监听地址
    • 其他元数据
  3. 信息处理:接收方处理收到的身份信息,用于:
    • 了解对方的能力和特性
    • 验证对方的身份
    • 优化后续的通信策略
  4. 信息更新:当节点信息发生变化时,主动推送更新后的信息给已连接的节点

Identify 协议对于构建可互操作的 P2P 网络至关重要,它使得节点能够了解彼此的能力和特性,为后续的通信和协作奠定基础。

3.5 Identify 协议详细工作原理

Identify 协议概述: Identify 协议是 libp2p 中的一个核心协议,用于节点之间交换身份信息和能力声明。它在节点建立连接后自动运行,确保节点能够了解彼此的能力和特性,为后续的通信和协作奠定基础。

协议流程

  1. 协议启动:当两个节点建立连接后,Identify 协议自动启动
  2. 信息收集:每个节点收集自己的身份信息,包括协议版本、支持的协议、公钥、监听地址等
  3. 信息发送:节点向对方发送身份信息
  4. 信息验证:接收方验证收到的身份信息的真实性
  5. 信息处理:接收方处理收到的身份信息,更新本地节点信息
  6. 信息更新:当节点信息发生变化时,主动推送更新后的信息

协议消息格式

  • Identify:包含节点身份信息的消息
  • IdentifyPush:主动推送的身份信息更新

libp2p 中的实现

  • 使用 identify::Behaviour 实现 Identify 协议
  • 自动处理信息的收集、发送和接收
  • 提供事件机制,通知应用程序身份信息的变化

3.6 身份信息结构

身份信息组成

  1. 协议版本:标识节点使用的 libp2p 版本
  2. 节点公钥:用于身份验证和加密通信
  3. 支持的协议:节点支持的所有协议列表
  4. 监听地址:节点的网络监听地址
  5. 观察地址:其他节点观察到的本节点地址
  6. 协议版本字符串:应用程序自定义的版本信息

信息结构示例

struct IdentifyInfo {
  protocol_version: String,
  agent_version: String,
  public_key: PublicKey,
  listen_addrs: Vec<Multiaddr>,
  observed_addr: Multiaddr,
  protocols: Vec<String>,
}

信息使用场景

  • 能力协商:根据对方支持的协议选择合适的通信协议
  • 地址发现:发现节点的其他网络地址
  • 身份验证:验证节点的身份真实性
  • 版本兼容性:检查协议版本兼容性

3.7 信息验证机制

验证流程

  1. 签名验证:验证身份信息的数字签名
  2. 公钥验证:验证公钥的有效性
  3. 地址验证:验证节点地址的可达性
  4. 协议验证:验证协议版本的兼容性

验证方法

  • 数字签名:使用节点的私钥对身份信息进行签名
  • 公钥验证:使用公钥验证签名的有效性
  • 地址探测:尝试连接节点的监听地址
  • 协议协商:尝试使用对方支持的协议进行通信

安全考虑

  • 身份伪造:通过数字签名防止身份信息被伪造
  • 地址欺骗:通过地址探测验证地址的真实性
  • 协议劫持:通过协议协商确保使用安全的协议

3.8 信息更新策略

更新触发条件

  1. 监听地址变化:当节点的监听地址发生变化时
  2. 支持协议变化:当节点支持的协议发生变化时
  3. 身份信息变化:当节点的其他身份信息发生变化时
  4. 定期更新:定期推送身份信息,确保信息的新鲜度

更新机制

  • 主动推送:当信息变化时主动推送更新
  • 定期更新:按照配置的时间间隔定期更新
  • 按需更新:在特定事件触发时更新

更新策略

  • 增量更新:只更新变化的部分,减少网络开销
  • 批量更新:将多个变化合并为一次更新
  • 优先级更新:优先更新重要的信息变化

3.9 性能影响

性能因素

  • 信息大小:身份信息的大小会影响传输时间和网络开销
  • 更新频率:更新频率过高会增加网络开销
  • 验证开销:信息验证会增加 CPU 开销
  • 存储开销:存储节点信息会增加内存开销

优化策略

  • 信息压缩:压缩身份信息,减少传输大小
  • 批量更新:合并多个更新,减少网络往返
  • 缓存机制:缓存节点信息,减少重复验证
  • 增量更新:只传输变化的部分,减少数据传输

性能权衡

  • 信息完整性 vs 网络开销:完整的身份信息提供更多上下文,但增加网络开销
  • 更新频率 vs 信息新鲜度:频繁更新保证信息新鲜度,但增加网络开销
  • 验证强度 vs 性能:强验证提高安全性,但增加 CPU 开销

3.10 实际应用场景

应用场景

  1. 节点发现:通过 Identify 协议发现节点的其他网络地址
  2. 协议协商:根据对方支持的协议选择合适的通信协议
  3. 负载均衡:根据节点的能力和状态进行负载均衡
  4. 安全认证:验证节点的身份,防止恶意节点
  5. 网络优化:根据节点的网络位置优化路由

实际案例

  • 区块链网络:使用 Identify 协议发现节点并验证身份
  • 分布式存储:根据节点能力分配存储任务
  • 实时通信:根据节点支持的协议选择最佳通信方式
  • IoT 网络:在资源受限的设备之间进行能力协商

最佳实践

  • 合理配置更新频率:根据网络环境和应用需求调整更新频率
  • 优化信息大小:只包含必要的身份信息,减少传输开销
  • 实现缓存机制:缓存节点信息,减少重复验证
  • 结合其他协议:与 Kademlia DHT 等协议结合,提高节点发现效率

3.11 运行项目

在多个终端运行相同的程序:

# 在多个终端运行相同程序
cargo run --package identify

运行效果

  1. 节点启动后,会自动在本地网络中发现其他运行中的节点
  2. 发现新节点后,会尝试建立连接
  3. 连接建立后,节点之间会自动交换身份信息,包括协议版本、支持的协议、节点公钥等
  4. 你可以在终端中看到节点信息的交换过程,包括发送和接收身份信息的详细日志
  5. 节点之间仍然会继续发送 Ping 消息以保持连接活跃
  6. 当节点信息发生变化时,会主动推送更新后的信息给已连接的节点

3.12 测试场景

场景 1:本地网络测试

  • 在同一台机器上启动多个终端,运行 Identify 节点
  • 观察节点之间的自动发现、连接建立和身份信息交换过程
  • 测试节点启动顺序对发现和信息交换过程的影响

场景 2:多机器测试

  • 在同一局域网内的不同机器上运行 Identify 节点
  • 观察跨机器的节点发现、连接建立和身份信息交换
  • 测试网络拓扑对信息交换效率的影响

场景 3:节点信息变化测试

  • 启动多个节点,待它们相互发现并建立连接后
  • 模拟节点信息变化(例如修改监听地址)
  • 观察节点是否能主动推送更新后的信息给其他节点

3.13 常见问题与解决方案

问题1:身份信息交换失败

  • 原因:网络连接问题、节点配置错误或协议版本不兼容
  • 解决方案
    • 检查网络连接是否正常
    • 确保节点配置正确,特别是公钥和协议版本设置
    • 确保所有节点使用兼容的 libp2p 版本

问题2:无法识别其他节点的能力

  • 原因:身份信息交换不完整或格式错误
  • 解决方案
    • 检查 Identify 协议配置是否正确
    • 确保节点正确实现了 Identify 协议
    • 检查网络连接质量,确保信息完整传输

问题3:节点信息更新不及时

  • 原因:网络延迟、节点离线或信息推送失败
  • 解决方案
    • 检查网络连接质量
    • 确保节点保持在线状态
    • 实现信息更新的重试机制

问题4:身份验证失败

  • 原因:公钥不匹配、签名验证失败或身份信息被篡改
  • 解决方案
    • 确保使用正确的密钥对
    • 检查签名验证机制是否正常
    • 确保网络安全,防止信息被篡改