4. 第一个Fast DDS程序

在掌握了Fast DDS的核心概念与通信模型之后,我们正式进入第一个实战程序。本章将围绕“HelloWorld”这一经典示例,完整演示如何创建DomainParticipant(域参与者)、Publisher(发布者)与Subscriber(订阅者),并打通从数据发布到数据接收的完整链路。这个程序虽然简单,但它涵盖了Fast DDS编程的几乎所有关键步骤:类型支持(TypeSupport)、QoS策略配置、实体创建、数据写入与回调接收。理解并吃透这个最小闭环,后续所有复杂应用(如点对点通信、请求-响应模式、多Topic管理)都将在此基础上展开。

Fast DDS HelloWorld 通信链路与实体关系图 Domain ID = 0(默认域) DomainParticipant 0 participant_0 Publisher publisher_0 QoS: 默认可靠 DataWriter writer_0 Topic: HelloWorldTopic Topic: HelloWorldTopic Type: HelloWorld Key: 无(Best Effort) 数据类型: index + message DomainParticipant 1 participant_1 Subscriber subscriber_0 QoS: 默认可靠 DataReader reader_0 Topic: HelloWorldTopic write() deliver RTPS Discovery / SPDP + SEDP 数据流: HelloWorld(index=1, message="Hello DDS!") 发布端序列化 → RTPS封装 → 网络传输 → 订阅端反序列化 → 回调 发布路径 订阅路径 发现/元数据

上图展示了本章程序的核心架构:两个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注册类型到ParticipantHelloWorldPubSubType
TopicDataType序列化/反序列化实现HelloWorldPubSubType 内部实现

4.2 操作流程:从IDL定义到实体创建的五步法

编写Fast DDS程序遵循一套固定的流程,我们将其归纳为“五步法”。每一步都有明确的输入与输出,掌握这套流程后,面对任何自定义类型都能快速上手。

  1. 定义IDL文件:编写HelloWorld.idl,声明数据结构。例如:
// HelloWorld.idl
struct HelloWorld
{
    unsigned long index;
    string message;
};
  1. 生成类型支持代码:在终端执行 fastddsgen HelloWorld.idl,生成四个文件(.h/.cxx 和 PubSubTypes.h/.cxx)。
  2. 创建DomainParticipant:使用DomainParticipantFactory创建参与者,并注册类型支持对象。
  3. 创建Publisher/Subscriber及Writer/Reader:通过Participant的工厂方法创建对应实体,并配置QoS。
  4. 编写数据收发逻辑:发布端调用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配置,探讨如何根据业务场景选择合适的策略组合。