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
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
用户创建] 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的存在 ===
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) | 正常运行 |
关键理解:
- A和B一直周期性发送,不管有没有新节点加入
- C加入后立即发送(首次宣告),然后进入周期性发送
- 租约检测:如果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
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: 开始数据传输
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机制的原理、设计模式和实操调试方法。