4. 第一个Fast DDS程序
在掌握了Fast DDS的核心概念与通信模型之后,我们正式进入第一个实战程序。本章将围绕“HelloWorld”这一经典示例,完整演示如何创建DomainParticipant(域参与者)、Publisher(发布者)与Subscriber(订阅者),并打通从数据发布到数据接收的完整链路。这个程序虽然简单,但它涵盖了Fast DDS编程的几乎所有关键步骤:类型支持(TypeSupport)、QoS策略配置、实体创建、数据写入与回调接收。理解并吃透这个最小闭环,后续所有复杂应用(如点对点通信、请求-响应模式、多Topic管理)都将在此基础上展开。
上图展示了本章程序的核心架构:两个DomainParticipant分别运行在发布端与订阅端进程中,通过RTPS发现协议自动建立连接。发布端创建Publisher与DataWriter,订阅端创建Subscriber与DataReader,双方通过Topic名称与数据类型进行匹配。下面我们按照“类型定义 → 实体创建 → 数据收发 → 编译运行”的顺序逐步拆解。
4.1 核心原理:Fast DDS的实体层次与类型支持机制
Fast DDS的编程模型遵循DDS规范中的实体层次结构。每个DomainParticipant是域内的根实体,它管理着该参与者所有的Publisher、Subscriber、Topic以及对应的Writer/Reader。理解这个层次关系是编写正确程序的前提。
关键实体关系:
- DomainParticipant:域参与者,代表一个通信节点。它负责域发现、QoS策略继承、实体工厂等职责。一个进程可以创建多个Participant,但通常一个进程一个Participant即可。
- Publisher / Subscriber:分别是数据发布与订阅的管理器。它们本身不直接传输数据,而是作为DataWriter和DataReader的容器,并统一管理默认QoS。
- DataWriter / DataReader:真正执行数据写入与读取的实体。DataWriter将用户数据序列化后交给RTPS协议层发送;DataReader从网络接收数据并反序列化后回调用户代码。
- Topic:数据通道的标识。发布端与订阅端必须使用相同的Topic名称和数据类型才能建立连接。
在Fast DDS中,用户自定义数据类型必须通过IDL(Interface Definition Language)定义,然后由fastddsgen工具生成对应的支持代码。这些代码包括类型定义、序列化/反序列化函数、类型支持对象(TypeSupport)等。TypeSupport是连接用户类型与DDS运行时的重要桥梁,它告诉Fast DDS如何操作该类型的数据。
| 组件 | 作用 | 对应代码 |
|---|---|---|
| IDL文件 | 定义数据类型结构 | HelloWorld.idl |
| fastddsgen生成 | 生成类型支持代码 | HelloWorld.h / .cxx, HelloWorldPubSubTypes.h / .cxx |
| TypeSupport | 注册类型到Participant | HelloWorldPubSubType |
| TopicDataType | 序列化/反序列化实现 | HelloWorldPubSubType 内部实现 |
4.2 操作流程:从IDL定义到实体创建的五步法
编写Fast DDS程序遵循一套固定的流程,我们将其归纳为“五步法”。每一步都有明确的输入与输出,掌握这套流程后,面对任何自定义类型都能快速上手。
- 定义IDL文件:编写
HelloWorld.idl,声明数据结构。例如:
// HelloWorld.idl
struct HelloWorld
{
unsigned long index;
string message;
};
- 生成类型支持代码:在终端执行
fastddsgen HelloWorld.idl,生成四个文件(.h/.cxx 和 PubSubTypes.h/.cxx)。 - 创建DomainParticipant:使用
DomainParticipantFactory创建参与者,并注册类型支持对象。 - 创建Publisher/Subscriber及Writer/Reader:通过Participant的工厂方法创建对应实体,并配置QoS。
- 编写数据收发逻辑:发布端调用
write(),订阅端实现on_data_available()回调。
下面我们以发布端代码为例,展示核心创建过程(省略错误处理):
// 发布端关键代码
DomainParticipantQos pqos;
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, pqos);
if (participant_ == nullptr) { /* 错误处理 */ }
// 注册类型
HelloWorldPubSubType type;
type.register_type(participant_, "HelloWorld");
// 创建Topic
TopicQos tqos = participant_->get_default_topic_qos();
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", tqos);
// 创建Publisher
PublisherQos pubqos = participant_->get_default_publisher_qos();
publisher_ = participant_->create_publisher(pubqos);
// 创建DataWriter
DataWriterQos wqos = publisher_->get_default_datawriter_qos();
writer_ = publisher_->create_datawriter(topic_, wqos);
订阅端代码结构类似,区别在于创建Subscriber和DataReader,并设置监听器(Listener)来接收数据回调。
4.3 案例拆解:HelloWorld完整通信闭环
我们构建一个实际可运行的示例。假设有两个进程:HelloWorldPublisher 和 HelloWorldSubscriber。发布端每秒发送一条消息,订阅端收到后打印到控制台。
订阅端监听器实现:
class SubListener : public DataReaderListener
{
public:
void on_data_available(DataReader* reader) override
{
HelloWorld data;
SampleInfo info;
if (reader->take_next_sample(&data, &info) == ReturnCode_t::RETCODE_OK)
{
if (info.valid_data)
{
std::cout << "Received: index=" << data.index()
<< ", message=\"" << data.message() << "\"" << std::endl;
}
}
}
};
发布端发送逻辑:
HelloWorld msg;
msg.index(0);
msg.message("Hello DDS!");
for (int i = 0; i < 10; ++i)
{
msg.index(i);
writer_->write(&msg);
std::this_thread::sleep_for(std::chrono::seconds(1));
}
运行结果(订阅端控制台输出):
Received: index=0, message="Hello DDS!"
Received: index=1, message="Hello DDS!"
...
Received: index=9, message="Hello DDS!"
这个案例验证了完整的通信链路。值得注意的是,take_next_sample() 返回的 SampleInfo 中的 valid_data 字段用于区分有效数据与生命周期管理信号(如dispose)。在实际工程中,必须检查该字段,否则可能处理到无效数据。
4.4 常见误区与总结复盘
初学者在编写第一个Fast DDS程序时,经常遇到以下几类问题。我们将其整理为“避坑清单”,帮助大家快速定位问题。
高频错误与解决方案:
- 类型未注册:创建Topic时提示“Type not registered”。原因是没有调用
register_type(),或注册名称与Topic创建时使用的类型名不一致。 - QoS不匹配:发布端与订阅端无法连接。最常见的是Reliability(可靠/尽力)或Durability(持久性)设置不一致。建议初学阶段使用默认QoS,或显式设置为
RELIABLE_RELIABILITY_QOS。 - Topic名称拼写错误:两端Topic名称必须完全一致,包括大小写。
- 忘记调用
take_next_sample():如果监听器回调触发了,但数据一直不更新,检查是否在回调中调用了take操作。Fast DDS不会自动清除缓存,必须手动取走数据。 - 跨网络通信失败:默认配置下,Fast DDS使用UDP多播进行发现。如果两端在不同网段,需要配置
XML配置文件指定单播地址或共享内存传输。
总结复盘:本章的HelloWorld程序虽然简单,但已经覆盖了Fast DDS编程的80%核心API。从IDL定义到实体创建,再到数据收发,每一步都体现了DDS规范的设计思想。建议读者在运行成功后,尝试修改以下参数观察变化:
- 将Reliability改为
BEST_EFFORT,观察丢包行为。 - 增加多个DataWriter或DataReader,验证多对多通信。
- 在同一个Participant中创建多个Topic,理解实体复用。
这些实验将帮助你从“会跑通”进阶到“懂原理”。下一阶段,我们将深入QoS配置,探讨如何根据业务场景选择合适的策略组合。