3. Fast DDS入门:从安装到第一个HelloWorld

各位同学,今天我们来聊聊Fast DDS。说实话,我第一次接触DDS的时候也被它的概念绕得头晕。但后来我发现,把它当成一个「超级快递系统」就好理解了——你要寄快递(发布数据),快递员(DDS中间件)负责送到收件人手里(订阅数据),而QoS就是你对快递服务的各种要求。

好,我们直接进入正题。

3.1 Fast DDS的安装与编译

安装Fast DDS,我建议你直接用源码编译。为什么?因为我在项目中遇到过好几次,用apt安装的版本太老,跟ROS2的某些特性不兼容,折腾半天才发现是版本问题。

⚠️ 注意: 如果你用的是Ubuntu 20.04或22.04,请确保系统已经安装了编译工具链。缺少依赖是新手最常见的坑。

3.1.1 安装依赖

sudo apt update
sudo apt install -y \
  cmake \
  g++ \
  git \
  libssl-dev \
  libasio-dev \
  libtinyxml2-dev \
  libp11-dev \
  softhsm2 \
  libsqlite3-dev \
  libgtest-dev \
  libgmock-dev

嗯,这里要特别提一下libasio-dev。我记得有一次帮同事排查问题,他编译一直报错,最后发现是asio版本太旧。所以建议你装完后再检查一下版本:

dpkg -l | grep libasio

3.1.2 编译Fast DDS

我个人习惯把源码放在~/ros2_ws/src下面,这样跟ROS2的工作空间统一管理。

cd ~/ros2_ws/src
git clone https://github.com/eProsima/Fast-DDS.git -b v2.10.0
cd Fast-DDS
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=/usr/local
make -j$(nproc)
sudo make install

编译时间大概5-10分钟,取决于你的机器性能。这时候可以去泡杯咖啡。

💡 小技巧: 如果你只想测试功能,可以用 make -j4 限制并行编译数,避免把CPU跑满导致电脑卡死。我吃过这个亏,编译到一半电脑直接死机...

3.1.3 验证安装

装完后,跑一下这个命令看看版本号:

fastdds --version

如果看到类似 Fast DDS version 2.10.0 的输出,恭喜你,安装成功了。

3.2 Fast DDS HelloWorld示例

好了,环境搭好了,我们来写第一个程序。说白了就是:一个发布者发「Hello World」,一个订阅者收「Hello World」。

3.2.1 定义数据类型

先用IDL定义我们的消息结构:

// HelloWorld.idl
struct HelloWorld
{
    unsigned long index;
    string message;
};

然后用Fast DDS提供的工具生成代码:

fastddsgen HelloWorld.idl

这会生成 HelloWorld.h、HelloWorld.cxx 等文件。你想想看,如果没有这个工具,手动写序列化代码得多痛苦。

3.2.2 编写发布者

// HelloWorldPublisher.cpp
#include "HelloWorld.h"
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/publisher/DataWriterListener.hpp>

using namespace eprosima::fastdds::dds;

class HelloWorldPublisher
{
private:
    DomainParticipant* participant_;
    Publisher* publisher_;
    DataWriter* writer_;
    Topic* topic_;
    HelloWorld hello_;

public:
    HelloWorldPublisher() : participant_(nullptr), publisher_(nullptr),
                            writer_(nullptr), topic_(nullptr)
    {
        hello_.index(0);
        hello_.message("Hello World");
    }

    bool init()
    {
        // 创建DomainParticipant
        DomainParticipantQos pqos;
        pqos.name("Participant_pub");
        participant_ = DomainParticipantFactory::get_instance()->create_participant(0, pqos);
        if (participant_ == nullptr) return false;

        // 注册类型
        TypeSupport type(new HelloWorldPubSubType());
        type.register_type(participant_);

        // 创建Topic
        topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
        if (topic_ == nullptr) return false;

        // 创建Publisher
        publisher_ = participant_->create_publisher(PUBLISHER_QOS_DEFAULT, nullptr);
        if (publisher_ == nullptr) return false;

        // 创建DataWriter
        writer_ = publisher_->create_datawriter(topic_, DATAWRITER_QOS_DEFAULT);
        if (writer_ == nullptr) return false;

        return true;
    }

    void publish()
    {
        hello_.index(hello_.index() + 1);
        writer_->write(&hello_);
        std::cout << "发送: " << hello_.message() << " #" << hello_.index() << std::endl;
    }
};

3.2.3 编写订阅者

// HelloWorldSubscriber.cpp
class HelloWorldSubscriber
{
private:
    DomainParticipant* participant_;
    Subscriber* subscriber_;
    DataReader* reader_;
    Topic* topic_;

public:
    bool init()
    {
        // 创建DomainParticipant
        DomainParticipantQos pqos;
        pqos.name("Participant_sub");
        participant_ = DomainParticipantFactory::get_instance()->create_participant(0, pqos);
        if (participant_ == nullptr) return false;

        // 注册类型
        TypeSupport type(new HelloWorldPubSubType());
        type.register_type(participant_);

        // 创建Topic(必须与发布者一致)
        topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);

        // 创建Subscriber
        subscriber_ = participant_->create_subscriber(SUBSCRIBER_QOS_DEFAULT, nullptr);

        // 创建DataReader并设置监听器
        reader_ = subscriber_->create_datareader(topic_, DATAREADER_QOS_DEFAULT, &listener_);
        return true;
    }
};
🔑 关键点: 发布者和订阅者的Topic名称、数据类型必须完全一致,否则它们无法通信。我见过有人把 "HelloWorldTopic" 写成了 "HelloWorld" ,结果数据死活收不到。

3.2.4 编译运行

g++ -std=c++11 -I/usr/local/include -L/usr/local/lib \
    HelloWorldPublisher.cpp HelloWorld.cxx \
    -lfastdds -lfastcdr -lpthread \
    -o publisher

g++ -std=c++11 -I/usr/local/include -L/usr/local/lib \
    HelloWorldSubscriber.cpp HelloWorld.cxx \
    -lfastdds -lfastcdr -lpthread \
    -o subscriber

先启动订阅者,再启动发布者:

./subscriber
# 另一个终端
./publisher

如果看到订阅者终端打印出 收到: Hello World #1,说明通信成功了。

3.3 Fast DDS的QoS策略配置

QoS是DDS的灵魂。说白了,它决定了你的数据怎么传、传多快、丢了怎么办。

3.3.1 常用QoS策略

QoS策略 作用 我常用的场景
RELIABILITY 可靠性:RELIABLE(可靠)或 BEST_EFFORT(尽力) 控制指令用RELIABLE,传感器数据用BEST_EFFORT
DURABILITY 持久性:TRANSIENT_LOCAL(保留最新数据给后加入的订阅者) 机器人状态信息用这个,新节点加入能拿到当前状态
HISTORY 历史记录:KEEP_LAST(保留最近N个)或 KEEP_ALL(保留所有) 日志数据用KEEP_ALL,实时控制用KEEP_LAST
DEADLINE 截止时间:数据必须在指定时间内更新 电机控制周期必须严格,超时就报警

3.3.2 配置示例

我曾经在一个项目中,需要确保机器人状态信息不丢失,同时新加入的节点能立即获取当前状态。配置如下:

// 配置DataWriter的QoS
DataWriterQos wqos;
wqos.reliability().kind = RELIABLE_RELIABILITY_QOS;  // 可靠传输
wqos.durability().kind = TRANSIENT_LOCAL_DURABILITY_QOS;  // 持久化
wqos.history().kind = KEEP_LAST_HISTORY_QOS;  // 保留最近
wqos.history().depth = 5;  // 保留5个样本
wqos.deadline().period.seconds = 1;  // 每秒至少更新一次

// 配置DataReader的QoS
DataReaderQos rqos;
rqos.reliability().kind = RELIABLE_RELIABILITY_QOS;
rqos.durability().kind = TRANSIENT_LOCAL_DURABILITY_QOS;
rqos.history().kind = KEEP_LAST_HISTORY_QOS;
rqos.history().depth = 5;
rqos.deadline().period.seconds = 1;
⚠️ 避坑指南: 我曾经把发布者配成RELIABLE,订阅者配成BEST_EFFORT,结果数据时有时无。记住:发布者和订阅者的QoS必须兼容,否则通信会失败或行为异常。

3.3.3 QoS不兼容的情况

举个例子,如果发布者要求RELIABLE,但订阅者只支持BEST_EFFORT,DDS会怎么处理?

答案是:它会尝试协商。如果协商失败,通信就建立不起来。你可以在监听器的 on_offered_deadline_missed 或 on_requested_deadline_missed 回调中捕获这些错误。

知识体系总览

下面这张图帮你理清Fast DDS的核心脉络:

Fast DDS 知识体系 安装与编译 依赖安装 → 源码编译 → 验证 HelloWorld示例 IDL定义 → 发布者 → 订阅者 QoS策略配置 可靠性 · 持久性 · 历史记录 核心概念:DomainParticipant → Topic → Publisher/Subscriber → DataWriter/DataReader 底层依赖:Fast CDR(序列化) + Asio(网络) + OpenSSL(安全) 机器人通信 自动驾驶 工业物联网

这张图把Fast DDS的三大块串起来了:先装好环境,再跑通HelloWorld,最后用QoS调优。每一步都是下一步的基础。

💡 我的建议: 初学者先别急着调QoS,把HelloWorld跑通再说。等你理解了发布-订阅的基本流程,再回头研究QoS的各种参数。我当年就是太心急,一上来就配各种QoS,结果连基本通信都没搞明白。

好了,Fast DDS的入门就到这里。记住:安装要稳,示例要通,QoS要懂。这三步走完,你就正式踏入DDS的大门了。

可可精品资料库