第三章 Fast-DDS核心源码导读与流程拆解-Discovery机制

1.3 补充章节:Discovery机制深度解析

章节状态:进行中

更新时间:2026-05-21

关联主章节:1.3 核心源码导读与流程拆解


STAR法则概述

要素 说明 在本章中的体现
Situation(背景) 什么场景下需要Discovery? 分布式系统自动发现节点
Task(任务) 要解决什么问题? 节点自动发现、动态扩缩容
Action(行动) 如何实现? SPDP/EDP协议、状态机、设计模式
Result(结果) 达到什么效果? 即插即用、零配置部署

一、Situation(背景与场景)

1.1 分布式系统的发现难题

传统方案的痛点:

方案 问题 适用场景
硬编码IP地址 节点变动需重新配置 静态小规模集群
配置文件 配置同步困难 中等规模集群
中心注册表(如etcd) 单点故障、额外依赖 云原生环境

DDS Discovery的独特价值:

graph LR A[新节点加入] --> B{传统方案} B --> C[修改配置] C --> D[重启服务] D --> E[分钟级上线] A --> F{DDS Discovery} F --> G[自动发现] G --> H[秒级上线] style F fill:#90EE90

1.2 Fast-DDS Discovery的商用场景

场景 挑战 Discovery解决方案
自动驾驶车队 车辆动态加入/离开 PDP自动发现新车辆
无人机集群 编队飞行时节点变化 实时感知节点上下线
工业产线 设备热插拔 即插即用通信
云边协同 边缘节点动态注册 跨网络发现

二、Task(任务与目标)

2.1 Discovery要解决的核心问题

graph TD subgraph "Discovery核心任务" T1[1. 谁在线?
Participant Discovery] T2[2. 谁能和谁通信?
Endpoint Discovery] T3[3. 如何联系?
Locator交换] T4[4. 还活着吗?
Lease检测] T1 --> T2 --> T3 --> T4 end

2.2 设计目标

目标 要求 指标
自动化 零配置发现 无需人工配置IP
实时性 快速感知变化 发现延迟 < 1秒
可扩展性 支持大规模集群 1000+节点
可靠性 网络抖动容错 自动重连

三、Action(实现方案)

3.1 整体架构

3.1.1 重要概念澄清:用户Writer vs 内部发现协议Writer

在理解Discovery架构前,必须区分两个层面的Writer/Reader:

层面 名称 创建者 用途 生命周期
用户层 DataWriter/DataReader 用户代码 传输业务数据 由用户控制
内部层 SPDP Writer/Reader SEDP Writer/Reader Fast-DDS内部 传输发现协议消息 随Participant自动创建/销毁

关键理解:

  • SPDP和EDP 本身也需要Writer/Reader来传输发现消息
  • 这些Writer/Reader是Fast-DDS 内部实现细节,对用户透明
  • 用户只感知到"自动发现",不感知内部的Writer/Reader
3.1.2 架构图
graph TB subgraph "Discovery架构" direction TB App[应用层代码] subgraph "用户DDS层" DP[DomainParticipant] T[Topic] DW[DataWriter
用户创建] DR[DataReader
用户创建] end subgraph "RTPS Discovery层(内部实现)" subgraph "PDP组件" PDP[PDP
Participant Discovery] SPDP_W[PDPStatelessWriter
内部创建] SPDP_R[StatelessReader
内部创建] end subgraph "EDP组件" EDP[EDP
Endpoint Discovery] SEDP_W[StatefulWriter
内部创建] SEDP_R[StatefulReader
内部创建] end end subgraph "传输抽象层 [Fast-DDS]" direction TB UDP[UDP Transport
跨机通信] SHM[SHM Transport
同机通信] TCP[TCP Transport
可靠跨机] end subgraph "操作系统网络层 [OS Kernel]" direction TB OS_UDP[Socket API
UDP/TCP] OS_SHM[共享内存机制
/dev/shm] OS_NET[网络协议栈
IP/以太网] end App -->|create_participant| DP DP -->|create_topic| T T -->|create_datawriter| DW T -->|create_datareader| DR DP -.->|init创建| PDP PDP -.->|create_spdp_endpoints| SPDP_W PDP -.->|create_spdp_endpoints| SPDP_R DW -.->|注册到| EDP DR -.->|注册到| EDP EDP -.->|create_sedp_endpoints| SEDP_W EDP -.->|create_sedp_endpoints| SEDP_R SPDP_W -->|发送SPDP消息| UDP SPDP_R -->|接收SPDP消息| UDP SEDP_W -->|发送SEDP消息| UDP SEDP_R -->|接收SEDP消息| UDP UDP -->|调用| OS_UDP SHM -->|调用| OS_SHM TCP -->|调用| OS_UDP OS_UDP -->|网络包| OS_NET end style SPDP_W fill:#ffe1e1 style SPDP_R fill:#ffe1e1 style SEDP_W fill:#e1f5ff style SEDP_R fill:#e1f5ff style DW fill:#90EE90 style DR fill:#90EE90

图例说明:

  • 🟢 绿色:用户创建的实体(DataWriter/DataReader)
  • 🔴 红色:PDP内部创建的实体(SPDP StatelessWriter/Reader)
  • 🔵 蓝色:EDP内部创建的实体(SEDP StatefulWriter/Reader)
  • 虚线箭头:内部自动创建,用户无感知
  • 实线箭头:用户主动调用API
3.1.3 各阶段创建的实体对比
阶段 用户操作 内部自动创建的实体 源码位置
SPDP create_participant() PDPStatelessWriter StatelessReader src/cpp/rtps/builtin/discovery/participant/simple/PDPSimple.cpp:358-415
EDP create_datawriter() create_datareader() StatefulWriter (SEDP Publications) StatefulReader (SEDP Publications) StatefulWriter (SEDP Subscriptions) StatefulReader (SEDP Subscriptions) src/cpp/rtps/builtin/discovery/endpoint/EDPSimple.cpp
3.1.4 为什么PDP用StatelessWriter,EDP用StatefulWriter?
特性 PDP (SPDP) EDP (SEDP)
通信模式 多播广播(一对所有) 单播点对点(一对一)
Writer类型 PDPStatelessWriter StatefulWriter
原因 广播场景不需要维护每个Reader的状态 需要可靠传输,维护每个Reader的ACK状态
可靠性 周期性心跳,丢包可容忍 必须可靠送达,支持重传
源码定义 src/cpp/rtps/builtin/discovery/participant/simple/PDPStatelessWriter.hpp:38 src/cpp/rtps/writer/StatefulWriter.hpp

源码证据:

cpp 复制代码
// PDPSimple创建的是PDPStatelessWriter(广播用)
// 文件:src/cpp/rtps/builtin/discovery/participant/simple/PDPSimple.cpp:415
writer.writer_ = dynamic_cast<PDPStatelessWriter*>(rtps_writer);

// EDPSimple创建的是StatefulWriter(可靠传输用)
// 文件:src/cpp/rtps/builtin/discovery/endpoint/EDPSimple.h
StatefulWriter* publications_writer_;   // SEDP Publications Writer
StatefulWriter* subscriptions_writer_;  // SEDP Subscriptions Writer

3.2 SPDP(Simple Participant Discovery Protocol)

3.2.1 协议设计

设计模式:心跳广播模式(Heartbeat Broadcast)

📌 图例说明:本图展示SPDP核心流程,如需了解Locator交换、心跳周期等细节,请参考下方子章节。

sequenceDiagram autonumber box Fast-DDS 层 participant A as 🔵 Participant A
192.168.1.10 participant TA as 🔵 UDP Transport-A participant TB as 🟢 UDP Transport-B participant B as 🟢 Participant B
192.168.1.11 participant TC as 🔴 UDP Transport-C participant C as 🔴 Participant C
192.168.1.12 end box OS 网络层 participant Net as ⚫ 多播239.255.0.1:7400 end Note over A,C: === 【阶段1】A和B的周期性心跳广播 [详见3.2.3] === rect rgb(230, 240, 255) Note over A,TA: 🔵 A的心跳流程 A->>TA: SPDP_DATA(A)
含metatraffic_locators [详见3.2.4] TA->>Net: UDP多播 Net->>TB: 转发 Net->>TC: 转发 TB->>B: 接收SPDP_DATA(A) TC->>C: 接收SPDP_DATA(A) end rect rgb(230, 255, 230) Note over B,TB: 🟢 B的心跳流程 B->>TB: SPDP_DATA(B)
含metatraffic_locators [详见3.2.4] TB->>Net: UDP多播 Net->>TA: 转发 Net->>TC: 转发 TA->>A: 接收SPDP_DATA(B) TC->>C: 接收SPDP_DATA(B) end Note over A,C: === 【阶段2】新节点🔴 C加入 === rect rgb(255, 230, 230) Note over C,TC: 🔴 C首次宣告 C->>TC: SPDP_DATA(C)
含metatraffic_locators [详见3.2.4] TC->>Net: UDP多播 Net->>TA: 转发到A Net->>TB: 转发到B TA->>A: 接收SPDP_DATA(C) TB->>B: 接收SPDP_DATA(C) end Note over A,C: === 【阶段3】🔵 A和🟢 B响应🔴 C(单播)=== rect rgb(230, 240, 255) Note over A,TA: 🔵 A响应C A->>TA: SPDP_DATA(A) 单播给C TA->>Net: UDP单播→192.168.1.12 Net->>TC: 转发 TC->>C: 接收SPDP_DATA(A) end rect rgb(230, 255, 230) Note over B,TB: 🟢 B响应C B->>TB: SPDP_DATA(B) 单播给C TB->>Net: UDP单播→192.168.1.12 Net->>TC: 转发 TC->>C: 接收SPDP_DATA(B) end Note over A,C: === 结果:🔴 C知道了🔵 A和🟢 B的存在 ===

图例导读:

标记 含义 详细说明位置
[详见3.2.3] 周期性心跳机制 第3.2.3节
[详见3.2.4] Locator交换细节 第3.2.4节
3.2.2 源码实现

关键类:PDPSimple

cpp 复制代码
// src/cpp/rtps/builtin/discovery/participant/PDPSimple.h
class PDPSimple : public PDP {
public:
    // 初始化:创建SPDP Writer/Reader
    bool init(RTPSParticipantImpl* part) override;
    
    // 周期性宣告
    void announce_participant_state(bool new_change);
    
    // 处理接收到的SPDP消息
    void process_participant_discovery(
        const ParticipantProxyData& pdata);
    
    // 清理过期Participant
    void remove_remote_participants();
    
private:
    // SPDP Writer:发送自己的存在宣告
    PDPStatelessWriter* mp_SPDPWriter;
    
    // SPDP Reader:接收其他Participant的宣告
    StatelessReader* mp_SPDPReader;
    
    // 定时器:周期性发送
    TimedEvent* mp_announcement_periodic;
};

设计模式应用:观察者模式

classDiagram class PDPListener { <> +on_new_cache_change_added() } class PDPSimpleListener { +on_new_cache_change_added() } class StatefulReader { +add_listener(listener) } PDPListener <|-- PDPSimpleListener PDPSimpleListener --> StatefulReader : 监听SPDP消息
3.2.3 周期性心跳机制详解

核心参数:

参数 默认值 说明 配置位置
announcement_period 3秒 SPDP消息发送间隔 PDPSimple::announcement_period
lease_duration 20秒 租约超时时间 ParticipantProxyData::m_leaseDuration

时间线示例:

css 复制代码
时间轴(秒):
0       3       6       9       12      15      18      21      24
|-------|-------|-------|-------|-------|-------|-------|-------|
A发送    A发送    A发送    A发送    A发送    A发送    A发送    A发送
B发送    B发送    B发送    B发送    B发送    B发送    B发送    B发送
                
                C在10秒时加入:
                        10      12      15      18      21
                        |       |       |       |       |
                        C首次   C发送   C发送   C发送   C发送
                        宣告

早已经接入的A和B的行为:

时间 A的行为 B的行为 C的行为 说明
0s 发送SPDP(A) 发送SPDP(B) - 正常运行
3s 发送SPDP(A) 发送SPDP(B) - 正常运行
6s 发送SPDP(A) 发送SPDP(B) - 正常运行
9s 发送SPDP(A) 发送SPDP(B) - 正常运行
10s 收到SPDP(C) 收到SPDP(C) 首次发送SPDP(C) C加入,A/B立即知道
12s 发送SPDP(A) 发送SPDP(B) 发送SPDP(C) 都进入周期性发送
15s 发送SPDP(A) 发送SPDP(B) 发送SPDP(C) 正常运行
18s 发送SPDP(A) 发送SPDP(B) 发送SPDP(C) 正常运行
21s 发送SPDP(A) 发送SPDP(B) 发送SPDP(C) 正常运行

关键理解:

  1. A和B一直周期性发送,不管有没有新节点加入
  2. C加入后立即发送(首次宣告),然后进入周期性发送
  3. 租约检测:如果20秒内没收到某个节点的SPDP消息,认为该节点离线

租约检测机制:

css 复制代码
Participant A的视角:
时间:  0s  3s  6s  9s  12s  15s  18s  21s  24s  27s  30s
        |   |   |   |   |    |    |    |    |    |    |
收到B: ✓   ✓   ✓   ✓   ✓    ✓    ✓    ✓    ✓    ✗    ✗
                                              ↑
                                        超过lease_duration(20s)
                                        认为B离线
3.2.4 Locator交换机制详解

Locator是DDS中用于标识通信地址的核心概念,SPDP阶段主要交换metatraffic_locators(发现协议通信地址)。

Locator定义:

cpp 复制代码
// src/cpp/fastdds/rtps/common/Locator.h
struct Locator {
    int32_t kind;           // 地址类型(UDPv4/UDPv6/SHM等)
    uint32_t port;          // 端口号
    octet address[16];      // IP地址(IPv4用前4字节)
};

Locator类型对比:

Locator类型 用途 默认端口计算 交换时机
metatraffic_locators Discovery通信(SPDP/EDP) 7400 + DomainID*2 SPDP阶段
default_locators 用户数据传输 7401 + DomainID*2 EDP阶段
builtin_locators 内置协议(同机SHM) /dev/shm/fastrtps_* 自动检测

Locator交换流程:

sequenceDiagram autonumber participant A as Participant A participant Net as 网络 participant B as Participant B Note over A,B: === SPDP阶段:交换Discovery地址 === A->>Net: SPDP_DATA(A)
metatraffic_locators=[192.168.1.10:7410] Net->>B: 转发 B->>B: 记录A的Discovery地址
用于后续EDP通信 B->>Net: SPDP_DATA(B)
metatraffic_locators=[192.168.1.11:7410] Net->>A: 转发 A->>A: 记录B的Discovery地址 Note over A,B: === EDP阶段:交换数据通信地址 === A->>B: SEDP_DATA(Writer)
default_locators=[192.168.1.10:7411] B->>B: 记录A的数据地址 B->>A: SEDP_DATA(Reader)
default_locators=[192.168.1.11:7411] A->>A: 记录B的数据地址 Note over A,B: === 数据传输:直接点对点通信 === A->>B: 用户数据
直接发送到192.168.1.11:7411

为什么需要Locator交换?

类比:交换名片

  • SPDP交换"公司前台电话"(Discovery地址)→ 用于发现彼此
  • EDP交换"部门直线电话"(数据地址)→ 用于直接高效通信
  • 数据传输直接拨打"直线电话",不再经过前台

Locator在SPDP消息中的位置:

cpp 复制代码
// src/cpp/rtps/builtin/data/ParticipantProxyData.h
struct ParticipantProxyData {
    GUID_t m_guid;                          // Participant唯一标识
    LocatorList m_metatrafficLocators;      // ✅ Discovery通信地址
    LocatorList m_defaultLocators;          // 数据通信地址(EDP阶段交换)
    Duration_t m_leaseDuration;             // 租约时长
    // ...
};
3.2.5 SPDP消息格式
cpp 复制代码
// src/cpp/rtps/builtin/data/ParticipantProxyData.h
struct ParticipantProxyData {
    // 核心标识
    GUID_t m_guid;                    // Participant全局唯一ID
    uint32_t m_domainId;              // Domain ID(隔离不同域)
    
    // 通信地址
    LocatorList m_metatrafficLocators;  // Discovery通信地址
    LocatorList m_defaultLocators;       // 数据通信地址
    
    // 协议版本
    ProtocolVersion_t m_protocolVersion;
    VendorId_t m_vendorId;
    
    // 租约信息(用于存活检测)
    Duration_t m_leaseDuration;       // 租约时长(默认20秒)
    
    // QoS策略
    ParticipantQos m_qos;
};
3.2.5 周期性心跳源码实现
cpp 复制代码
// src/cpp/rtps/builtin/discovery/participant/simple/PDPSimple.cpp

// 周期性发送定时器回调
void PDPSimple::announce_participant_state(bool new_change) {
    // 构造SPDP消息
    ParticipantProxyData spdp_data;
    spdp_data.m_guid = participant_->get_guid();
    spdp_data.m_domainId = domain_id_;
    spdp_data.m_leaseDuration = lease_duration_;
    // ... 填充其他字段
    
    // 发送SPDP消息(多播给所有Participant)
    mp_SPDPWriter->write(&spdp_data);
    
    // 重新调度定时器(默认3秒后再次执行)
    mp_announcement_periodic->restart_timer();
}

// 租约检测定时器回调(清理过期Participant)
void PDPSimple::remove_remote_participants() {
    auto now = std::chrono::steady_clock::now();
    
    for (auto it = remote_participants_.begin(); 
         it != remote_participants_.end(); ) {
        
        // 计算距离上次收到心跳的时间
        auto elapsed = now - it->second.last_seen;
        
        if (elapsed > it->second.lease_duration) {
            // 超过lease_duration未收到心跳,认为节点离线
            log_info("Participant %s lease expired, removing", 
                     it->second.guid.to_string().c_str());
            
            // 通知应用层节点离线
            notify_participant_removed(it->second.guid);
            
            // 从列表中移除
            it = remote_participants_.erase(it);
        } else {
            ++it;
        }
    }
}

3.3 EDP(Endpoint Discovery Protocol)

3.3.1 协议设计

设计模式:发布订阅模式(Pub-Sub)

sequenceDiagram autonumber participant WA as Writer A participant SEDP_W as SEDP Writer participant SEDP_R as SEDP Reader participant RB as Reader B Note over WA,RB: === DataWriter创建 === WA->>WA: create_datawriter(Topic="vehicle") WA->>SEDP_W: 宣告新Writer Note over WA,RB: === SEDP传播 === SEDP_W->>SEDP_R: DATA(w)
Writer信息 Note over WA,RB: === DataReader创建 === RB->>RB: create_datareader(Topic="vehicle") RB->>SEDP_R: 宣告新Reader Note over WA,RB: === 匹配 === SEDP_R->>SEDP_R: 检测Topic匹配 SEDP_R->>WA: 通知:有匹配的Reader SEDP_R->>RB: 通知:有匹配的Writer WA->>RB: 开始数据传输
3.3.2 源码实现

关键类:EDPSimple

cpp 复制代码
// src/cpp/rtps/builtin/discovery/endpoint/EDPSimple.h
class EDPSimple : public EDP {
public:
    // 初始化SEDPPublicationsWriter/Reader
    bool init(RTPSParticipantImpl* part) override;
    
    // 发布DataWriter信息
    bool process_local_writer_matching(DataWriter* writer);
    
    // 发布DataReader信息
    bool process_local_reader_matching(DataReader* reader);
    
    // 处理远程Writer宣告
    bool process_remote_writer_matching(
        const WriterProxyData& wdata);
    
    // 处理远程Reader宣告
    bool process_remote_reader_matching(
        const ReaderProxyData& rdata);
    
    // Topic匹配算法
    bool matching_compatible(
        const WriterProxyData& wdata,
        const ReaderProxyData& rdata);
        
private:
    // SEDP Publications Writer:宣告本地DataWriter
    StatefulWriter* publications_writer_;
    
    // SEDP Publications Reader:接收远程DataWriter信息
    StatefulReader* publications_reader_;
    
    // SEDP Subscriptions Writer:宣告本地DataReader
    StatefulWriter* subscriptions_writer_;
    
    // SEDP Subscriptions Reader:接收远程DataReader信息
    StatefulReader* subscriptions_reader_;
};
3.3.3 Topic匹配算法
cpp 复制代码
// EDP匹配逻辑(简化)
bool EDP::matching_compatible(
    const WriterProxyData& writer,
    const ReaderProxyData& reader)
{
    // 1. Topic名称必须相同
    if (writer.topic_name() != reader.topic_name()) {
        return false;
    }
    
    // 2. 数据类型必须兼容
    if (!type_compatible(writer.type(), reader.type())) {
        return false;
    }
    
    // 3. QoS策略必须兼容
    if (!qos_compatible(writer.qos(), reader.qos())) {
        return false;
    }
    
    // 4. Domain必须相同(已在PDP阶段过滤)
    
    return true;
}

3.4 状态机设计

Participant Discovery状态机:

stateDiagram-v2 [*] --> INITIAL: 创建Participant INITIAL --> ANNOUNCING: 启动SPDP Writer ANNOUNCING --> DISCOVERING: 发送首个SPDP消息 DISCOVERING --> DISCOVERED: 收到其他Participant SPDP DISCOVERED --> DISCOVERING: 继续宣告自己 DISCOVERING --> READY: 发现所有已知节点 DISCOVERED --> READY: EDP完成匹配 READY --> DISCOVERING: 新节点加入 READY --> DISCOVERING: 节点离线重连 DISCOVERING --> FAILED: 网络异常 DISCOVERED --> FAILED: 租约超时 FAILED --> DISCOVERING: 自动重试 FAILED --> [*]: 超过重试次数 READY --> [*]: 销毁Participant

3.5 设计模式总结

设计模式 应用场景 源码体现
观察者模式 SPDP消息监听 PDPListener监听StatefulReader
状态机模式 Discovery生命周期 Participant状态转换
工厂模式 创建Discovery组件 PDPFactory创建PDP实例
策略模式 不同Discovery策略 PDPSimple vs PDPServer
单例模式 DomainParticipantFactory 全局唯一工厂

四、Result(效果与验证)

4.1 预期效果

指标 目标值 实测方法
发现延迟 < 1秒 抓包测量SPDP间隔
支持规模 1000+节点 压力测试
网络开销 < 1%带宽 多播流量统计
故障恢复 < 5秒 模拟网络分区

4.2 调试与观测实操

4.2.1 gdb调试:SPDP状态机

编译(Debug模式):

bash 复制代码
cd /home/guang/code/opensource/Fast-DDS
mkdir -p build && cd build
cmake .. -DCMAKE_BUILD_TYPE=Debug -DCOMPILE_EXAMPLES=ON
make hello_world -j$(nproc)

调试SPDP流程:

bash 复制代码
gdb ./examples/cpp/hello_world/hello_world

# 断点1:PDP初始化
gdb> break eprosima::fastrtps::rtps::PDPSimple::init

# 断点2:SPDP周期性宣告
gdb> break eprosima::fastrtps::rtps::PDPSimple::announce_participant_state

# 断点3:接收SPDP消息
gdb> break eprosima::fastrtps::rtps::PDPSimpleListener::on_new_cache_change_added

# 断点4:处理远程Participant
gdb> break eprosima::fastrtps::rtps::PDPSimple::process_participant_discovery

# 断点5:租约检测(存活检查)
gdb> break eprosima::fastrtps::rtps::PDPSimple::remove_remote_participants

gdb> run

观察要点:

断点 观察变量 理解目标
init mp_SPDPWriter, mp_SPDPReader 确认SPDP组件创建
announce_participant_state mp_announcement_periodic 确认定时器周期(默认3秒)
on_new_cache_change_added change->writerGUID 确认收到哪个Participant的消息
process_participant_discovery pdata.m_guid, pdata.m_leaseDuration 确认解析的Participant信息
remove_remote_participants expired_participants 确认租约超时检测
4.2.2 tcpdump抓包:观察SPDP多播

抓包命令:

bash 复制代码
# 抓取SPDP多播流量(默认239.255.0.1:7400)
sudo tcpdump -i lo -w spdp.pcap host 239.255.0.1 and port 7400

# 同时启动发布端和订阅端
./examples/cpp/hello_world/hello_world publisher &
./examples/cpp/hello_world/hello_world subscriber &

# 停止抓包后分析

Wireshark分析:

ini 复制代码
# 过滤SPDP消息
rtps.sm.id == 0x15 && rtps.participant

# 观察字段:
# - participant_guid: Participant唯一标识
# - domain_id: Domain隔离
# - metatraffic_unicast_locator: 单播发现地址
# - lease_duration: 租约时长
4.2.3 strace观察:网络系统调用
bash 复制代码
# 观察SPDP相关的socket操作
strace -f -e trace=socket,bind,setsockopt,sendto,recvfrom \
    ./examples/cpp/hello_world/hello_world 2>&1 | tee spdp_strace.log

# 关键观察:
# 1. socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP) - 创建UDP socket
# 2. setsockopt(..., IP_ADD_MEMBERSHIP, ...) - 加入多播组
# 3. sendto(..., 239.255.0.1, ...) - 发送SPDP多播
# 4. recvfrom(...) - 接收SPDP响应
4.2.4 系统监控:/dev/shm观察
bash 复制代码
# 监控共享内存(同机发现时使用SHM)
watch -n 0.5 'ls -la /dev/shm/ | grep fastrtps'

# 观察发现相关的SHM文件
# fastrtps_port7410 - SPDP多播端口
# fastrtps_port7411 - SEDP单播端口
4.2.5 日志分析:Fast-DDS内置日志
bash 复制代码
# 设置日志级别为Debug
export FASTDDS_DEFAULT_PROFILES_FILE=debug_profile.xml

# debug_profile.xml内容:
# <?xml version="1.0" encoding="UTF-8" ?>
# <dds>
#     <log>
#         <verbosity>DEBUG</verbosity>
#     </log>
# </dds>

# 运行并观察日志
./examples/cpp/hello_world/hello_world 2>&1 | grep -E "PDP|EDP|Discovery"

4.3 常见问题诊断

问题现象 诊断方法 解决思路
节点互相发现不了 tcpdump抓SPDP多播 检查防火墙、多播是否开启
发现但无法通信 gdb断点EDP::matching_compatible 检查Topic名称、类型、QoS
节点频繁上下线 gdb断点remove_remote_participants 调整lease_duration
大规模集群发现慢 统计SPDP消息频率 使用Discovery Server模式

五、配置管理最佳实践

5.1 防止错误订阅的设计原则

Discovery机制只解决"找到谁",不解决"找对没"。业务层需要保证订阅正确性。

5.1.1 命名空间隔离
xml 复制代码
<!-- 按系统/子系统/节点命名 -->
<topic_name>drone/001/telemetry</topic_name>
<topic_name>drone/001/command</topic_name>
<topic_name>vehicle/001/status</topic_name>
<topic_name>ground_station/monitor</topic_name>

命名规范:

层级 含义 示例
第1级 系统类型 drone, vehicle, ground_station
第2级 节点ID 001, 002, ...
第3级 数据类型 telemetry, command, status
5.1.2 Domain ID隔离
cpp 复制代码
// 不同系统使用不同Domain,彻底隔离
enum class SystemDomain : uint32_t {
    DRONE_FLEET = 1,      // 无人机集群
    VEHICLE_FLEET = 2,    // 车辆集群
    GROUND_CONTROL = 3,   // 地面控制站
    SIMULATION = 99       // 仿真环境
};

// 无人机节点
domain_participant = factory->create_participant(
    static_cast<uint32_t>(SystemDomain::DRONE_FLEET));

// 车辆节点  
domain_participant = factory->create_participant(
    static_cast<uint32_t>(SystemDomain::VEHICLE_FLEET));

// 不同Domain之间无法通信,即使Topic名称相同
5.1.3 配置文件模板化
xml 复制代码
<!-- drone_template.xml -->
<dds>
    <domain_participant name="${NODE_NAME}" domain_id="${DOMAIN_ID}">
        <subscriber name="${NODE_NAME}_sub">
            <!-- 只允许订阅drone命名空间 -->
            <data_reader name="telemetry_reader">
                <topic_name>drone/${NODE_ID}/telemetry</topic_name>
            </data_reader>
            <data_reader name="command_reader">
                <topic_name>drone/${NODE_ID}/command</topic_name>
            </data_reader>
        </subscriber>
        
        <publisher name="${NODE_NAME}_pub">
            <data_writer name="status_writer">
                <topic_name>drone/${NODE_ID}/status</topic_name>
            </data_writer>
        </publisher>
    </domain_participant>
</dds>

配置生成脚本:

bash 复制代码
#!/bin/bash
# generate_config.sh

NODE_NAME=$1  # drone_001
NODE_ID=$2    # 001
DOMAIN_ID=1

sed -e "s/\${NODE_NAME}/${NODE_NAME}/g" \
    -e "s/\${NODE_ID}/${NODE_ID}/g" \
    -e "s/\${DOMAIN_ID}/${DOMAIN_ID}/g" \
    drone_template.xml > ${NODE_NAME}_config.xml

# 验证配置
grep -E "vehicle|car" ${NODE_NAME}_config.xml
if [ $? -eq 0 ]; then
    echo "错误:配置中包含车辆相关Topic!"
    exit 1
fi

5.2 运行时监控:检测错误订阅

5.2.1 监控Discovery匹配事件
cpp 复制代码
// 自定义Discovery监控Listener
class DiscoveryMonitor : public DomainParticipantListener {
public:
    void on_publication_matched(DataWriter* writer,
        const PublicationMatchedStatus& info) override {
        
        std::string topic_name = writer->get_topic()->get_name();
        
        // 检查是否匹配了预期外的Topic
        if (!is_expected_topic(topic_name)) {
            log_error("异常匹配:Writer %s 匹配了非预期Topic %s",
                writer->guid().to_string().c_str(),
                topic_name.c_str());
            
            // 发送告警
            send_alert("DISCOVERY_ANOMALY", topic_name);
        }
    }
    
    void on_subscription_matched(DataReader* reader,
        const SubscriptionMatchedStatus& info) override {
        
        std::string topic_name = reader->get_topic()->get_name();
        
        // 检查订阅是否正确
        if (!is_expected_subscription(topic_name)) {
            log_error("异常订阅:Reader %s 订阅了非预期Topic %s",
                reader->guid().to_string().c_str(),
                topic_name.c_str());
            
            // 可选:主动断开连接
            // reader->get_subscriber()->delete_datareader(reader);
        }
    }
    
private:
    bool is_expected_topic(const std::string& topic) {
        // 检查Topic是否符合命名规范
        return topic.find("drone/") == 0 ||  // 只允许drone命名空间
               topic.find("system/") == 0;    // 或系统级Topic
    }
};
5.2.2 使用gdb检测运行时订阅
bash 复制代码
# 启动应用并附加gdb
gdb -p $(pgrep drone_node)

# 设置断点:创建Topic时检查名称
gdb> break eprosima::fastdds::dds::DomainParticipant::create_topic

# 当断点命中时,检查topic_name参数
gdb> p topic_name
# 预期:"drone/001/telemetry"
# 异常:"vehicle/status"  ← 发现错误!

# 查看调用栈,定位配置来源
gdb> bt
# 可以看到是哪个配置文件或代码创建的Topic
5.2.3 日志审计:记录所有Discovery事件
cpp 复制代码
// 启用Fast-DDS详细日志
export FASTDDS_DEFAULT_PROFILES_FILE=audit_profile.xml
xml 复制代码
<!-- audit_profile.xml -->
<dds>
    <log>
        <verbosity>DEBUG</verbosity>
        <category>DISCOVERY</category>
    </log>
    
    <domain_participant>
        <property>
            <name>dds.discovery.audit</name>
            <value>true</value>
        </property>
    </domain_participant>
</dds>

审计日志分析:

bash 复制代码
# 提取所有Discovery事件
grep "DISCOVERY" fastdds.log | tee discovery_audit.log

# 分析异常匹配
grep -E "vehicle|car" discovery_audit.log
# 如果发现无人机节点有vehicle相关日志,说明配置错误

# 统计各节点订阅的Topic
awk '/on_subscription_matched/ {print $5}' discovery_audit.log | sort | uniq -c

5.3 DDS-Security权限控制

5.3.1 基于证书的Topic访问控制
xml 复制代码
<!-- permissions.xml -->
<dds xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
    <permissions>
        <grant name="DroneNodePermissions">
            <subject_name>CN=drone_001,O=MyOrg</subject_name>
            
            <!-- 允许加入的Domain -->
            <validity>
                <not_before>2024-01-01T00:00:00</not_before>
                <not_after>2025-12-31T23:59:59</not_after>
            </validity>
            
            <!-- 允许的Topic权限 -->
            <allow_rule>
                <domain_id>1</domain_id>
                <publish>
                    <topic>drone/001/*</topic>  <!-- 只允许发布drone/001/下的Topic -->
                </publish>
                <subscribe>
                    <topic>drone/001/*</topic>  <!-- 只允许订阅drone/001/下的Topic -->
                    <topic>ground_station/command</topic>  <!-- 可以接收地面站指令 -->
                </subscribe>
            </allow_rule>
            
            <!-- 明确禁止 -->
            <deny_rule>
                <domain_id>2</domain_id>  <!-- 禁止加入车辆Domain -->
            </deny_rule>
        </grant>
    </permissions>
</dds>
5.3.2 权限验证流程
sequenceDiagram autonumber participant Node as 无人机节点 participant Auth as Authentication插件 participant AC as AccessControl插件 participant DDS as Fast-DDS Node->>Auth: 发送证书(CN=drone_001) Auth->>Auth: 验证证书合法性 Auth->>AC: 查询drone_001的权限 AC->>AC: 加载permissions.xml AC->>AC: 检查:允许订阅drone/* ? alt 权限检查通过 AC->>DDS: 授权加入Domain 1 Node->>DDS: 正常通信 else 尝试订阅vehicle/* AC->>DDS: 拒绝订阅请求 DDS->>Node: 返回RETCODE_NOT_ALLOWED end
5.3.3 调试权限控制
bash 复制代码
# 启用Security调试日志
export FASTDDS_SECURITY_DEBUG=1

# 运行应用,观察权限检查
./drone_node 2>&1 | grep -E "AccessControl|Permission"

# 预期输出:
# [AccessControl] Checking permission for topic: drone/001/telemetry
# [AccessControl] ALLOWED
# 
# 异常输出:
# [AccessControl] Checking permission for topic: vehicle/status
# [AccessControl] DENIED

六、对二次开发的启示

6.1 自定义Discovery策略

场景:通过etcd实现中心式发现

cpp 复制代码
// 继承PDP基类
class PDPEtcd : public PDP {
public:
    bool init(RTPSParticipantImpl* part) override {
        // 连接etcd
        etcd_client_.connect("etcd-server:2379");
        
        // 注册自己
        etcd_client_.put("/dds/participants/" + guid_, participant_info_);
        
        // 监听其他节点
        etcd_client_.watch("/dds/participants/", 
            [this](const std::string& key, const std::string& value) {
                process_participant_discovery(parse(value));
            });
        
        return true;
    }
    
private:
    EtcdClient etcd_client_;
};

6.2 Discovery性能优化

优化点 方法 效果
减少多播风暴 使用Discovery Server 支持10000+节点
降低发现延迟 调整announcement_period 发现延迟<100ms
减少内存占用 限制Participant缓存数量 降低30%内存

六、学习检查清单

检查项 掌握标准
SPDP vs EDP区别 能解释Participant发现和Endpoint发现的分层设计
多播地址计算 能根据Domain ID计算多播地址和端口
租约机制 能解释lease_duration的作用和检测原理
Topic匹配 能列出匹配的三个条件
状态机 能画出Participant Discovery状态转换图
调试能力 能用gdb跟踪SPDP消息发送和接收流程
抓包分析 能用Wireshark分析SPDP消息内容

七、参考资源

资源 路径/链接
SPDP源码 src/cpp/rtps/builtin/discovery/participant/PDPSimple.cpp
EDP源码 src/cpp/rtps/builtin/discovery/endpoint/EDPSimple.cpp
发现数据 src/cpp/rtps/builtin/data/ParticipantProxyData.cpp
RTPS Spec OMG官方文档 8.5节 Discovery
Wireshark RTPS wiki.wireshark.org/RTPS

本补充章节按照STAR法则编写,深入解析Discovery机制的原理、设计模式和实操调试方法。

相关推荐
Oneslide1 小时前
fio测试导致磁盘LVM结构损坏故障处置办法
后端
小小前端仔LC1 小时前
Node.js + LangChain +React:搭建个人知识库(四)- 把向量和文件切块存入mysql中
后端·node.js
神奇小汤圆1 小时前
Agent 框架别急着乱学:先用 LangChain 搞懂 7 个基本模块
后端
什么半岛铁盒1 小时前
LangChain 入门与架构:快速搭建你的第一个 AI 应用
人工智能·架构·langchain
mirror_zAI1 小时前
C++ 仿 QQ 聊天室项目:Qt 客户端 + epoll 服务端 + Reactor 架构(含源码)
c++·qt·架构
神奇小汤圆1 小时前
一次线上故障带你看懂 MySQL InnoDB 缓冲池
后端
啷里格啷2 小时前
第三章 Fast-DDS核心源码导读与流程拆解
后端·架构
爱编程的小新☆2 小时前
Spring-AI入门
java·后端·spring
蝎子莱莱爱打怪2 小时前
👋🏻👋🏻再见,拉勾网——那个"最懂互联网人"的招聘平台倒了😭
前端·后端·招聘