第 2 部分:添加 mDNS 节点发现

0 阅读12分钟

本部分在基础 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:?}"),
      _ => {}
    }
  }
}

代码详细解析:

  1. 自定义网络行为

    • 创建 MyBehavior 结构体,组合了 ping::Behaviourmdns::tokio::Behaviour
    • 使用 #[derive(swarm::NetworkBehaviour)] 宏自动实现 NetworkBehaviour trait
    • 指定 to_swarm 类型为 MyBehaviourEvent,用于统一事件处理
  2. 事件枚举

    • 定义 MyBehaviourEvent 枚举,包含 PingMdns 两个变体
    • 每个变体对应一种行为产生的事件
  3. 事件转换

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

    • 创建 mdns::tokio::Behaviour 实例,使用默认配置
    • 传入本地节点 ID,用于在网络中识别自己
    • mDNS 行为会自动在本地网络中广播和接收发现消息
  5. 传输层配置

    • 与基础 Ping 节点类似,但添加了 20 秒的超时设置
    • 这有助于处理网络不稳定的情况
  6. 事件处理

    • Ping 事件:处理 Ping 响应,显示成功或失败信息,包括 RTT(往返时间)
    • mDNS 事件
      • Discovered:发现新节点时,获取节点 ID 和地址,避免连接到自己,尝试建立连接
      • Expired:节点离线时,记录离线信息
    • 连接事件:处理连接建立、关闭、错误等情况
    • 其他事件:处理传入连接、拨号等事件

2.4 mDNS 工作原理

mDNS 是一种基于多播的 DNS 协议,其工作原理如下:

  1. 服务发现:节点在本地网络中发送多播 DNS 查询,寻找其他 P2P 节点
  2. 服务响应:收到查询的节点发送响应,包含自己的节点 ID 和网络地址
  3. 服务缓存:节点缓存发现的服务信息,定期刷新
  4. 服务过期:如果长时间没有收到某个节点的响应,将其标记为离线

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 记录描述服务

工作流程

  1. 查询阶段:节点发送多播查询消息,询问特定类型的服务
  2. 响应阶段:提供相应服务的节点发送单播响应消息
  3. 确认阶段:查询节点收到响应后,可能发送确认消息
  4. 缓存阶段:节点缓存发现的服务信息,定期刷新

libp2p 中的 mDNS 实现

  • 使用 mdns::tokio::Behaviour 实现 mDNS 功能
  • 自动在本地网络中广播和接收发现消息
  • 支持服务发现和服务公告
  • 提供事件机制,通知应用程序发现和过期的服务

2.6 网络行为组合模式

组合模式概述: 在 libp2p 中,网络行为(NetworkBehaviour)可以通过组合多个简单行为来构建复杂行为。这种组合模式允许开发者模块化地构建 P2P 应用,每个行为负责特定的功能。

实现方法

  1. 创建组合结构体:定义一个包含多个行为的结构体
  2. 使用宏派生:使用 #[derive(swarm::NetworkBehaviour)] 宏自动实现 NetworkBehaviour trait
  3. 定义事件枚举:创建一个枚举类型,包含所有子行为的事件
  4. 实现事件转换:为每个子行为的事件实现 From trait,转换为组合事件

示例代码

#[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 事件处理机制

事件处理流程

  1. 事件产生:各个网络行为产生自己的事件
  2. 事件转换:通过 From trait 将子行为事件转换为组合事件
  3. 事件传递:Swarm 将组合事件传递给应用层
  4. 事件处理:应用层根据事件类型进行处理

事件类型

  • 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 直接发现

解决方案

  1. mDNS 中继:在不同网络之间设置 mDNS 中继服务
  2. 边界路由器:配置支持 mDNS 转发的边界路由器
  3. 混合发现:结合 mDNS 和其他发现机制(如 Kademlia DHT)
  4. 手动配置:对于跨网络场景,使用手动配置的引导节点

实际应用建议

  • 本地网络:使用 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

运行效果

  1. 节点启动后,会自动在本地网络中发现其他运行中的节点
  2. 发现新节点后,会尝试建立连接
  3. 连接建立后,节点之间会相互发送 Ping 消息并收到 Pong 响应
  4. 当节点离线时,mDNS 会检测到并记录相关信息
  5. 整个过程无需手动配置连接地址,实现了真正的自组织网络

2.12 测试场景

场景 1:本地网络测试

  • 在同一台机器上启动多个终端,运行 mDNS 节点
  • 观察节点之间的自动发现和连接过程
  • 测试节点启动顺序对发现过程的影响

场景 2:多机器测试

  • 在同一局域网内的不同机器上运行 mDNS 节点
  • 观察跨机器的节点发现和连接
  • 测试网络拓扑对发现效率的影响

场景 3:节点离线测试

  • 启动多个节点,待它们相互发现并建立连接后
  • 关闭其中一个节点
  • 观察其他节点是否能检测到该节点离线

2.13 常见问题与解决方案

问题1:mDNS 发现失败

  • 原因:网络防火墙阻止多播流量、网络隔离或 mDNS 配置问题
  • 解决方案
    • 检查防火墙设置,确保允许 UDP 5353 端口的多播流量
    • 确保所有节点在同一局域网内
    • 检查网络是否支持多播

问题2:节点发现但连接失败

  • 原因:网络连接问题、端口被占用或节点配置错误
  • 解决方案
    • 检查网络连接是否正常
    • 确保节点监听的端口未被占用
    • 检查节点配置是否正确

问题3:节点频繁离线和在线

  • 原因:网络不稳定、mDNS 超时设置过短或节点负载过高
  • 解决方案
    • 检查网络连接质量
    • 调整 mDNS 配置中的超时参数
    • 确保节点资源充足

问题4:mDNS 只发现部分节点

  • 原因:网络分段、多播范围限制或节点配置差异
  • 解决方案
    • 确保所有节点在同一网络段
    • 检查网络设备是否限制了多播范围
    • 确保所有节点使用相同的 mDNS 配置