本部分在 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:?}"),
_ => {}
}
}
}
代码详细解析:
-
扩展网络行为:
- 在
MyBehavior结构体中添加了identify::Behaviour,实现节点身份信息交换 - 使用
#[derive(swarm::NetworkBehaviour)]宏自动实现NetworkBehaviourtrait - 组合了
ping::Behaviour、mdns::tokio::Behaviour和identify::Behaviour
- 在
-
扩展事件枚举:
- 在
MyBehaviourEvent中添加了Identify变体,用于处理 Identify 事件 - 现在事件枚举包含
Ping、Mdns和Identify三个变体
- 在
-
事件转换:
- 实现
From<identify::Event> for MyBehaviourEventtrait,将 Identify 事件转换为自定义事件 - 保持之前的
From<ping::Event>和From<mdns::Event>实现 - 这样可以统一处理来自不同行为的事件
- 实现
-
Identify 初始化:
- 创建
identify::Behaviour实例,配置协议版本和节点公钥 - 协议版本设置为 "demo/1.0.0",用于标识应用程序类型和版本
- 传入节点的公钥,用于身份验证和识别
- 创建
-
传输层配置:
- 与 mDNS 示例类似,但添加了 60 秒的超时设置
- 这有助于处理网络不稳定的情况,特别是在节点信息交换过程中
-
事件处理:
- Identify 事件:
Received:收到其他节点的身份信息,包含协议版本、支持的协议、节点公钥等Sent:成功发送本节点的身份信息给其他节点Error:身份信息交换出错,可能是网络问题或节点配置问题Pushed:主动推送身份信息给其他节点,通常在节点信息发生变化时
- 其他事件:保持与之前示例相同的处理逻辑,包括 Ping 事件、mDNS 事件和连接事件
- Identify 事件:
3.4 Identify 协议工作原理
Identify 协议是 libp2p 中的一个核心协议,其工作原理如下:
- 连接建立:当两个节点建立连接后,Identify 协议自动启动
- 信息交换:节点相互发送自己的身份信息,包括:
- 协议版本
- 支持的协议列表
- 节点公钥
- 节点监听地址
- 其他元数据
- 信息处理:接收方处理收到的身份信息,用于:
- 了解对方的能力和特性
- 验证对方的身份
- 优化后续的通信策略
- 信息更新:当节点信息发生变化时,主动推送更新后的信息给已连接的节点
Identify 协议对于构建可互操作的 P2P 网络至关重要,它使得节点能够了解彼此的能力和特性,为后续的通信和协作奠定基础。
3.5 Identify 协议详细工作原理
Identify 协议概述: Identify 协议是 libp2p 中的一个核心协议,用于节点之间交换身份信息和能力声明。它在节点建立连接后自动运行,确保节点能够了解彼此的能力和特性,为后续的通信和协作奠定基础。
协议流程:
- 协议启动:当两个节点建立连接后,Identify 协议自动启动
- 信息收集:每个节点收集自己的身份信息,包括协议版本、支持的协议、公钥、监听地址等
- 信息发送:节点向对方发送身份信息
- 信息验证:接收方验证收到的身份信息的真实性
- 信息处理:接收方处理收到的身份信息,更新本地节点信息
- 信息更新:当节点信息发生变化时,主动推送更新后的信息
协议消息格式:
- Identify:包含节点身份信息的消息
- IdentifyPush:主动推送的身份信息更新
libp2p 中的实现:
- 使用
identify::Behaviour实现 Identify 协议 - 自动处理信息的收集、发送和接收
- 提供事件机制,通知应用程序身份信息的变化
3.6 身份信息结构
身份信息组成:
- 协议版本:标识节点使用的 libp2p 版本
- 节点公钥:用于身份验证和加密通信
- 支持的协议:节点支持的所有协议列表
- 监听地址:节点的网络监听地址
- 观察地址:其他节点观察到的本节点地址
- 协议版本字符串:应用程序自定义的版本信息
信息结构示例:
struct IdentifyInfo {
protocol_version: String,
agent_version: String,
public_key: PublicKey,
listen_addrs: Vec<Multiaddr>,
observed_addr: Multiaddr,
protocols: Vec<String>,
}
信息使用场景:
- 能力协商:根据对方支持的协议选择合适的通信协议
- 地址发现:发现节点的其他网络地址
- 身份验证:验证节点的身份真实性
- 版本兼容性:检查协议版本兼容性
3.7 信息验证机制
验证流程:
- 签名验证:验证身份信息的数字签名
- 公钥验证:验证公钥的有效性
- 地址验证:验证节点地址的可达性
- 协议验证:验证协议版本的兼容性
验证方法:
- 数字签名:使用节点的私钥对身份信息进行签名
- 公钥验证:使用公钥验证签名的有效性
- 地址探测:尝试连接节点的监听地址
- 协议协商:尝试使用对方支持的协议进行通信
安全考虑:
- 身份伪造:通过数字签名防止身份信息被伪造
- 地址欺骗:通过地址探测验证地址的真实性
- 协议劫持:通过协议协商确保使用安全的协议
3.8 信息更新策略
更新触发条件:
- 监听地址变化:当节点的监听地址发生变化时
- 支持协议变化:当节点支持的协议发生变化时
- 身份信息变化:当节点的其他身份信息发生变化时
- 定期更新:定期推送身份信息,确保信息的新鲜度
更新机制:
- 主动推送:当信息变化时主动推送更新
- 定期更新:按照配置的时间间隔定期更新
- 按需更新:在特定事件触发时更新
更新策略:
- 增量更新:只更新变化的部分,减少网络开销
- 批量更新:将多个变化合并为一次更新
- 优先级更新:优先更新重要的信息变化
3.9 性能影响
性能因素:
- 信息大小:身份信息的大小会影响传输时间和网络开销
- 更新频率:更新频率过高会增加网络开销
- 验证开销:信息验证会增加 CPU 开销
- 存储开销:存储节点信息会增加内存开销
优化策略:
- 信息压缩:压缩身份信息,减少传输大小
- 批量更新:合并多个更新,减少网络往返
- 缓存机制:缓存节点信息,减少重复验证
- 增量更新:只传输变化的部分,减少数据传输
性能权衡:
- 信息完整性 vs 网络开销:完整的身份信息提供更多上下文,但增加网络开销
- 更新频率 vs 信息新鲜度:频繁更新保证信息新鲜度,但增加网络开销
- 验证强度 vs 性能:强验证提高安全性,但增加 CPU 开销
3.10 实际应用场景
应用场景:
- 节点发现:通过 Identify 协议发现节点的其他网络地址
- 协议协商:根据对方支持的协议选择合适的通信协议
- 负载均衡:根据节点的能力和状态进行负载均衡
- 安全认证:验证节点的身份,防止恶意节点
- 网络优化:根据节点的网络位置优化路由
实际案例:
- 区块链网络:使用 Identify 协议发现节点并验证身份
- 分布式存储:根据节点能力分配存储任务
- 实时通信:根据节点支持的协议选择最佳通信方式
- IoT 网络:在资源受限的设备之间进行能力协商
最佳实践:
- 合理配置更新频率:根据网络环境和应用需求调整更新频率
- 优化信息大小:只包含必要的身份信息,减少传输开销
- 实现缓存机制:缓存节点信息,减少重复验证
- 结合其他协议:与 Kademlia DHT 等协议结合,提高节点发现效率
3.11 运行项目
在多个终端运行相同的程序:
# 在多个终端运行相同程序
cargo run --package identify
运行效果:
- 节点启动后,会自动在本地网络中发现其他运行中的节点
- 发现新节点后,会尝试建立连接
- 连接建立后,节点之间会自动交换身份信息,包括协议版本、支持的协议、节点公钥等
- 你可以在终端中看到节点信息的交换过程,包括发送和接收身份信息的详细日志
- 节点之间仍然会继续发送 Ping 消息以保持连接活跃
- 当节点信息发生变化时,会主动推送更新后的信息给已连接的节点
3.12 测试场景
场景 1:本地网络测试
- 在同一台机器上启动多个终端,运行 Identify 节点
- 观察节点之间的自动发现、连接建立和身份信息交换过程
- 测试节点启动顺序对发现和信息交换过程的影响
场景 2:多机器测试
- 在同一局域网内的不同机器上运行 Identify 节点
- 观察跨机器的节点发现、连接建立和身份信息交换
- 测试网络拓扑对信息交换效率的影响
场景 3:节点信息变化测试
- 启动多个节点,待它们相互发现并建立连接后
- 模拟节点信息变化(例如修改监听地址)
- 观察节点是否能主动推送更新后的信息给其他节点
3.13 常见问题与解决方案
问题1:身份信息交换失败
- 原因:网络连接问题、节点配置错误或协议版本不兼容
- 解决方案:
- 检查网络连接是否正常
- 确保节点配置正确,特别是公钥和协议版本设置
- 确保所有节点使用兼容的 libp2p 版本
问题2:无法识别其他节点的能力
- 原因:身份信息交换不完整或格式错误
- 解决方案:
- 检查 Identify 协议配置是否正确
- 确保节点正确实现了 Identify 协议
- 检查网络连接质量,确保信息完整传输
问题3:节点信息更新不及时
- 原因:网络延迟、节点离线或信息推送失败
- 解决方案:
- 检查网络连接质量
- 确保节点保持在线状态
- 实现信息更新的重试机制
问题4:身份验证失败
- 原因:公钥不匹配、签名验证失败或身份信息被篡改
- 解决方案:
- 确保使用正确的密钥对
- 检查签名验证机制是否正常
- 确保网络安全,防止信息被篡改