本部分在基础 Ping 节点的基础上,添加了 mDNS 节点发现功能。mDNS(多播 DNS)是一种在本地网络中自动发现服务和设备的协议,允许 P2P 节点在无需中央服务器的情况下相互发现。通过本示例,你将学习如何组合多个 NetworkBehaviour,实现更复杂的 P2P 功能,以及如何处理多行为的事件。mDNS 是构建自组织 P2P 网络的重要组件,它使得节点能够在本地网络中自动发现彼此,无需手动配置连接地址。
核心功能和目标:
- 实现 mDNS 节点发现功能
- 组合多个 NetworkBehaviour
- 处理多行为的事件
- 构建自组织 P2P 网络
在整个项目中的位置和作用:
- 作为第二个实践示例
- 展示如何组合多个 NetworkBehaviour
- 为后续功能添加节点发现能力
- 实现无需中央服务器的节点发现
学习该部分的预期收益:
- 掌握 mDNS 协议的工作原理
- 学会组合多个 NetworkBehaviour
- 理解如何处理多行为的事件
- 为构建自组织 P2P 网络奠定基础
2.1 创建 mDNS 项目
在 crates 目录下创建一个新的子项目:
cd crates
cargo new mdns
2.2 修改 crates/mdns/Cargo.toml 文件
配置 mDNS 项目的依赖项:
[package]
name = "mdns"
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" # 网络行为宏
] }
依赖说明:
- mdns:提供 mDNS 节点发现功能
- macros:提供网络行为宏,用于组合多个行为
2.3 修改 crates/mdns/src/main.rs 文件
实现带有 mDNS 节点发现功能的 P2P 节点:
use std::time::Duration;
use libp2p::{
PeerId, Swarm, Transport, core::upgrade, futures::StreamExt, identity, mdns, noise, ping, swarm,
tcp, yamux,
};
#[derive(swarm::NetworkBehaviour)]
#[behaviour(to_swarm = "MyBehaviourEvent")]
struct MyBehavior {
ping: ping::Behaviour,
mdns: mdns::tokio::Behaviour,
}
// 2. 定义事件枚举
#[derive(Debug)]
enum MyBehaviourEvent {
Ping(ping::Event),
Mdns(mdns::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)
}
}
#[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(20))
.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(10)),
);
let behaviour = MyBehavior { ping, mdns };
let mut swarm = Swarm::new(
transport,
behaviour,
peer_id,
swarm::Config::with_tokio_executor().with_idle_connection_timeout(Duration::from_secs(30)),
);
swarm.listen_on("/ip4/0.0.0.0/tcp/0".parse()?)?;
tracing::info!("🚀 mDNS节点启动,自动发现本地网络中的节点...");
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);
}
}
},
},
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结构体,组合了ping::Behaviour和mdns::tokio::Behaviour - 使用
#[derive(swarm::NetworkBehaviour)]宏自动实现NetworkBehaviourtrait - 指定
to_swarm类型为MyBehaviourEvent,用于统一事件处理
- 创建
-
事件枚举:
- 定义
MyBehaviourEvent枚举,包含Ping和Mdns两个变体 - 每个变体对应一种行为产生的事件
- 定义
-
事件转换:
- 实现
From<ping::Event> for MyBehaviourEventtrait,将 Ping 事件转换为自定义事件 - 实现
From<mdns::Event> for MyBehaviourEventtrait,将 mDNS 事件转换为自定义事件 - 这样可以统一处理来自不同行为的事件
- 实现
-
mDNS 初始化:
- 创建
mdns::tokio::Behaviour实例,使用默认配置 - 传入本地节点 ID,用于在网络中识别自己
- mDNS 行为会自动在本地网络中广播和接收发现消息
- 创建
-
传输层配置:
- 与基础 Ping 节点类似,但添加了 20 秒的超时设置
- 这有助于处理网络不稳定的情况
-
事件处理:
- Ping 事件:处理 Ping 响应,显示成功或失败信息,包括 RTT(往返时间)
- mDNS 事件:
Discovered:发现新节点时,获取节点 ID 和地址,避免连接到自己,尝试建立连接Expired:节点离线时,记录离线信息
- 连接事件:处理连接建立、关闭、错误等情况
- 其他事件:处理传入连接、拨号等事件
2.4 mDNS 工作原理
mDNS 是一种基于多播的 DNS 协议,其工作原理如下:
- 服务发现:节点在本地网络中发送多播 DNS 查询,寻找其他 P2P 节点
- 服务响应:收到查询的节点发送响应,包含自己的节点 ID 和网络地址
- 服务缓存:节点缓存发现的服务信息,定期刷新
- 服务过期:如果长时间没有收到某个节点的响应,将其标记为离线
mDNS 的优点是无需中央服务器,节点可以在本地网络中自动发现彼此,简化了网络配置。
2.5 mDNS 协议深入解析
mDNS 协议概述: mDNS(多播 DNS)是一种在本地网络中实现服务发现的协议,它允许设备在无需中央 DNS 服务器的情况下相互发现。mDNS 使用 UDP 协议在本地网络中发送多播消息,实现设备之间的自动发现。
技术细节:
- 多播地址:使用 IPv4 多播地址 224.0.0.251 或 IPv6 多播地址 ff02::fb
- 端口:使用 UDP 端口 5353
- 消息格式:基于标准 DNS 消息格式,但有特定的 mDNS 扩展
- 服务记录:使用 SRV、TXT、A/AAAA 记录描述服务
工作流程:
- 查询阶段:节点发送多播查询消息,询问特定类型的服务
- 响应阶段:提供相应服务的节点发送单播响应消息
- 确认阶段:查询节点收到响应后,可能发送确认消息
- 缓存阶段:节点缓存发现的服务信息,定期刷新
libp2p 中的 mDNS 实现:
- 使用
mdns::tokio::Behaviour实现 mDNS 功能 - 自动在本地网络中广播和接收发现消息
- 支持服务发现和服务公告
- 提供事件机制,通知应用程序发现和过期的服务
2.6 网络行为组合模式
组合模式概述: 在 libp2p 中,网络行为(NetworkBehaviour)可以通过组合多个简单行为来构建复杂行为。这种组合模式允许开发者模块化地构建 P2P 应用,每个行为负责特定的功能。
实现方法:
- 创建组合结构体:定义一个包含多个行为的结构体
- 使用宏派生:使用
#[derive(swarm::NetworkBehaviour)]宏自动实现NetworkBehaviourtrait - 定义事件枚举:创建一个枚举类型,包含所有子行为的事件
- 实现事件转换:为每个子行为的事件实现
Fromtrait,转换为组合事件
示例代码:
#[derive(swarm::NetworkBehaviour)]
#[behaviour(to_swarm = "MyBehaviourEvent")]
struct MyBehavior {
ping: ping::Behaviour,
mdns: mdns::tokio::Behaviour,
}
#[derive(Debug)]
enum MyBehaviourEvent {
Ping(ping::Event),
Mdns(mdns::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)
}
}
优点:
- 模块化:每个行为负责特定功能,便于维护和测试
- 可扩展性:可以轻松添加或移除行为
- 代码复用:可以在不同项目中重用相同的行为组合
- 清晰的事件处理:通过枚举类型统一处理不同行为的事件
2.7 事件处理机制
事件处理流程:
- 事件产生:各个网络行为产生自己的事件
- 事件转换:通过
Fromtrait 将子行为事件转换为组合事件 - 事件传递:Swarm 将组合事件传递给应用层
- 事件处理:应用层根据事件类型进行处理
事件类型:
- mDNS 事件:
Discovered:发现新节点Expired:节点离线
- Ping 事件:
- 包含 Ping 结果和往返时间
- 连接事件:
ConnectionEstablished:连接建立ConnectionClosed:连接关闭IncomingConnection:收到传入连接OutgoingConnectionError: outgoing 连接错误
事件处理最佳实践:
- 使用 match 语句:根据事件类型进行不同处理
- 分层处理:将事件处理逻辑分层,提高代码可读性
- 错误处理:妥善处理事件处理过程中的错误
- 日志记录:记录重要事件,便于调试和监控
2.8 mDNS 配置选项
配置参数:
- 扫描间隔:控制 mDNS 扫描的频率
- 超时时间:控制服务过期的时间
- 多播接口:指定使用哪些网络接口进行多播
- 服务类型:指定要发现的服务类型
配置示例:
let mdns_config = mdns::Config {
ttl: Duration::from_secs(60), // 服务记录的生存时间
scan_interval: Duration::from_secs(10), // 扫描间隔
interface_ips: None, // 使用所有网络接口
};
let mdns = mdns::tokio::Behaviour::new(mdns_config, peer_id)?;
最佳配置实践:
- 扫描间隔:根据网络规模和设备数量调整,一般为 5-30 秒
- 超时时间:一般为扫描间隔的 3-5 倍
- 网络接口:在多网卡环境中,指定特定接口可以提高效率
2.9 跨网络发现
mDNS 跨网络限制:
- mDNS 通常只在本地网络中工作,因为多播数据包通常不会被路由器转发
- 不同子网之间的设备无法通过 mDNS 直接发现
解决方案:
- mDNS 中继:在不同网络之间设置 mDNS 中继服务
- 边界路由器:配置支持 mDNS 转发的边界路由器
- 混合发现:结合 mDNS 和其他发现机制(如 Kademlia DHT)
- 手动配置:对于跨网络场景,使用手动配置的引导节点
实际应用建议:
- 本地网络:使用 mDNS 进行自动发现
- 跨网络:使用 Kademlia DHT 或手动配置引导节点
- 混合场景:结合使用多种发现机制,提高可靠性
2.10 安全性考虑
mDNS 安全隐患:
- 信息泄露:mDNS 广播会暴露网络中的服务信息
- 服务欺骗:攻击者可以发送伪造的 mDNS 响应
- 拒绝服务:攻击者可以发送大量 mDNS 消息,造成网络拥塞
- 信息收集:攻击者可以通过 mDNS 发现网络中的设备和服务
防护措施:
- 网络隔离:在安全敏感环境中,隔离 mDNS 流量
- 防火墙设置:合理配置防火墙,限制 mDNS 流量
- 服务验证:对发现的服务进行验证,确保其真实性
- 加密通信:即使使用 mDNS 发现服务,后续通信仍应使用加密
- 速率限制:限制 mDNS 消息的发送速率,防止 DoS 攻击
libp2p 安全实践:
- 使用 Noise 协议进行加密通信
- 对节点身份进行验证
- 实现访问控制机制
- 定期更新 libp2p 版本,修复安全漏洞
2.11 运行项目
在多个终端运行相同的程序:
# 在多个终端运行相同程序
cargo run --package mdns
输出示例:
🔑 本地节点ID: 12D3KooWA...
👂 监听地址: /ip4/127.0.0.1/tcp/51234
🔍 发现新节点: 12D3KooWB...
🔍 发现新节点: 12D3KooWC...
✅ 连接建立: 12D3KooWB...
🏓 Ping成功: 12D3KooWB... RTT: 1.2ms
运行效果:
- 节点启动后,会自动在本地网络中发现其他运行中的节点
- 发现新节点后,会尝试建立连接
- 连接建立后,节点之间会相互发送 Ping 消息并收到 Pong 响应
- 当节点离线时,mDNS 会检测到并记录相关信息
- 整个过程无需手动配置连接地址,实现了真正的自组织网络
2.12 测试场景
场景 1:本地网络测试
- 在同一台机器上启动多个终端,运行 mDNS 节点
- 观察节点之间的自动发现和连接过程
- 测试节点启动顺序对发现过程的影响
场景 2:多机器测试
- 在同一局域网内的不同机器上运行 mDNS 节点
- 观察跨机器的节点发现和连接
- 测试网络拓扑对发现效率的影响
场景 3:节点离线测试
- 启动多个节点,待它们相互发现并建立连接后
- 关闭其中一个节点
- 观察其他节点是否能检测到该节点离线
2.13 常见问题与解决方案
问题1:mDNS 发现失败
- 原因:网络防火墙阻止多播流量、网络隔离或 mDNS 配置问题
- 解决方案:
- 检查防火墙设置,确保允许 UDP 5353 端口的多播流量
- 确保所有节点在同一局域网内
- 检查网络是否支持多播
问题2:节点发现但连接失败
- 原因:网络连接问题、端口被占用或节点配置错误
- 解决方案:
- 检查网络连接是否正常
- 确保节点监听的端口未被占用
- 检查节点配置是否正确
问题3:节点频繁离线和在线
- 原因:网络不稳定、mDNS 超时设置过短或节点负载过高
- 解决方案:
- 检查网络连接质量
- 调整 mDNS 配置中的超时参数
- 确保节点资源充足
问题4:mDNS 只发现部分节点
- 原因:网络分段、多播范围限制或节点配置差异
- 解决方案:
- 确保所有节点在同一网络段
- 检查网络设备是否限制了多播范围
- 确保所有节点使用相同的 mDNS 配置