4. Cyclone DDS入门:安装、编译与HelloWorld

聊完了DDS的基本概念,咱们得动真格的了。这一章我带你亲手搭建Cyclone DDS环境,跑通第一个示例。说实话,我当年刚接触DDS时,光安装就折腾了大半天。现在回想起来,很多坑其实可以避免。

4.1 为什么选Cyclone DDS?

ROS 2默认支持多种DDS实现,比如Fast DDS、RTI Connext、Eclipse Cyclone DDS。我个人习惯用Cyclone DDS,原因有三:

  • 开源且轻量:Eclipse基金会维护,代码干净,依赖少
  • 性能不错:延迟和吞吐量在同类产品中属于第一梯队
  • 与ROS 2集成好:从Foxy版本开始就是官方推荐之一

我在项目中遇到过几次因为DDS实现不同导致的兼容性问题,后来统一用Cyclone DDS,世界清净多了。

4.2 安装Cyclone DDS

安装方式有两种:源码编译和包管理器安装。我建议初学者先用包管理器,省心。

4.2.1 包管理器安装(Ubuntu 22.04)

sudo apt update
sudo apt install cyclonedds

嗯,就这么简单。不过要注意,这个包可能不是最新版。如果你需要最新特性,往下看源码编译。

4.2.2 源码编译安装

我个人更推荐源码编译,因为你能控制编译选项,还能调试。我曾经为了排查一个内存泄漏问题,不得不从源码开始一步步跟踪。

# 安装依赖
sudo apt install git cmake build-essential

# 克隆仓库
git clone https://github.com/eclipse-cyclonedds/cyclonedds.git
cd cyclonedds

# 创建构建目录
mkdir build && cd build

# 配置CMake
cmake -DCMAKE_INSTALL_PREFIX=/usr/local ..

# 编译(使用4个核心加速)
make -j4

# 安装
sudo make install
小提示:编译时如果报错缺少某些库,多半是依赖没装全。运行 sudo apt build-dep cyclonedds 可以自动安装所有编译依赖。

4.3 验证安装

装完了怎么知道好不好使?写个简单的C程序测试一下。

#include <stdio.h>
#include <dds/dds.h>

int main() {
    dds_entity_t participant;
    dds_entity_t topic;
    dds_entity_t writer;
    dds_entity_t reader;

    // 创建参与者
    participant = dds_create_participant(DDS_DOMAIN_DEFAULT, NULL, NULL);
    if (participant < 0) {
        fprintf(stderr, "创建参与者失败: %s\n", dds_strretcode(-participant));
        return 1;
    }

    // 创建主题
    topic = dds_create_topic(participant, &dds_topic_descriptor_t, "HelloWorld", NULL, NULL);
    if (topic < 0) {
        fprintf(stderr, "创建主题失败\n");
        return 1;
    }

    printf("Cyclone DDS 安装成功!\n");

    // 清理
    dds_delete(participant);
    return 0;
}

编译运行:

gcc -o test_dds test_dds.c -lddsc
./test_dds

看到"Cyclone DDS 安装成功!"就说明环境没问题了。

4.4 HelloWorld示例:发布-订阅模式

咱们来写一个完整的发布-订阅示例。这个例子我用了很多次,每次给团队新人培训都拿它当入门。

4.4.1 定义数据类型

先用IDL定义消息结构:

// HelloWorld.idl
module HelloWorld {
    struct Message {
        long id;
        string content;
    };
};

然后用Cyclone DDS自带的IDL编译器生成代码:

idlc -l c HelloWorld.idl

这会生成 HelloWorld.h 和 HelloWorld.c 两个文件。

4.4.2 发布者代码

// publisher.c
#include "HelloWorld.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>

int main() {
    dds_entity_t participant;
    dds_entity_t topic;
    dds_entity_t writer;
    HelloWorld_Message msg;
    dds_return_t ret;

    participant = dds_create_participant(DDS_DOMAIN_DEFAULT, NULL, NULL);
    topic = dds_create_topic(participant, &HelloWorld_Message_desc, "HelloWorldTopic", NULL, NULL);
    writer = dds_create_writer(participant, topic, NULL, NULL);

    printf("发布者启动,开始发送消息...\n");

    for (int i = 0; i < 10; i++) {
        msg.id = i;
        snprintf(msg.content, 256, "Hello World! 第%d条消息", i);
        ret = dds_write(writer, &msg);
        if (ret != DDS_RETCODE_OK) {
            fprintf(stderr, "发送失败: %s\n", dds_strretcode(-ret));
        }
        printf("已发送: id=%d, content=%s\n", msg.id, msg.content);
        sleep(1);
    }

    dds_delete(participant);
    return 0;
}

4.4.3 订阅者代码

// subscriber.c
#include "HelloWorld.h"
#include <stdio.h>

int main() {
    dds_entity_t participant;
    dds_entity_t topic;
    dds_entity_t reader;
    HelloWorld_Message *msg;
    void *samples[1];
    dds_sample_info_t info;
    dds_return_t ret;

    participant = dds_create_participant(DDS_DOMAIN_DEFAULT, NULL, NULL);
    topic = dds_create_topic(participant, &HelloWorld_Message_desc, "HelloWorldTopic", NULL, NULL);
    reader = dds_create_reader(participant, topic, NULL, NULL);

    printf("订阅者启动,等待接收消息...\n");

    while (1) {
        ret = dds_take(reader, samples, &info, 1, 1);
        if (ret > 0 && info.valid_data) {
            msg = (HelloWorld_Message*)samples[0];
            printf("收到消息: id=%d, content=%s\n", msg->id, msg->content);
        }
        usleep(100000); // 100ms
    }

    dds_delete(participant);
    return 0;
}

4.4.4 编译与运行

# 编译发布者
gcc -o publisher publisher.c HelloWorld.c -lddsc

# 编译订阅者
gcc -o subscriber subscriber.c HelloWorld.c -lddsc

# 先启动订阅者(在一个终端)
./subscriber

# 再启动发布者(在另一个终端)
./publisher

你会看到订阅者终端逐条打印出发布者发送的消息。这就是DDS最基本的发布-订阅通信。

核心要点:发布者和订阅者通过主题(Topic)关联,不需要知道对方的存在。这就是DDS的"去中心化"设计。

4.5 QoS策略配置

QoS(Quality of Service)是DDS的灵魂。说白了,就是告诉中间件"这个数据该怎么传"。我刚开始接触时觉得QoS参数太多记不住,后来发现常用的就那么几个。

4.5.1 常用QoS策略

QoS策略 作用 典型场景
RELIABILITY 可靠性:可靠/尽力 控制指令用可靠,传感器数据用尽力
DURABILITY 持久性:是否保存历史数据 晚加入的订阅者能否收到之前的数据
HISTORY 历史深度:保留多少条历史数据 需要回放场景时设置较大深度
DEADLINE 截止时间:数据必须在指定时间内到达 实时控制系统中必须按时到达

4.5.2 配置示例

咱们给之前的发布者加上QoS配置:

// 创建QoS对象
dds_qos_t *qos = dds_create_qos();

// 设置可靠性为可靠模式
dds_qset_reliability(qos, DDS_RELIABILITY_RELIABLE, DDS_SECS(1));

// 设置持久性为持久模式,保留最后5条数据
dds_qset_durability(qos, DDS_DURABILITY_TRANSIENT_LOCAL);
dds_qset_history(qos, DDS_HISTORY_KEEP_LAST, 5);

// 使用配置好的QoS创建写入器
writer = dds_create_writer(participant, topic, qos, NULL);

// 用完记得释放
dds_delete_qos(qos);
避坑指南:我曾经在项目中把RELIABILITY设成了尽力模式,结果控制指令偶尔丢失,机器人撞了墙才反应过来。从那以后,凡是控制相关的数据,我必用可靠模式。

4.5.3 QoS匹配规则

发布者和订阅者的QoS必须兼容才能通信。规则很简单:

  • 发布者要求可靠,订阅者可以接受可靠或尽力
  • 发布者要求尽力,订阅者只能接受尽力
  • 说白了,订阅者的要求不能比发布者更严格

这个规则我记了很久才记住。你想想看,发布者说"我发的东西丢了就丢了",订阅者却说"不行,你必须可靠地发给我",这肯定矛盾嘛。

4.6 本章知识体系

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

Cyclone DDS 知识体系 安装与编译 包管理器安装 | 源码编译 | 依赖管理 HelloWorld示例 IDL定义 | 发布者 | 订阅者 | 编译运行 QoS策略配置 RELIABILITY | DURABILITY | HISTORY | DEADLINE ROS 2集成与实战应用

从安装到HelloWorld再到QoS配置,这是一条清晰的进阶路径。我个人建议你先把HelloWorld跑通,再慢慢调QoS参数,感受不同配置带来的变化。

我的经验:调试QoS时,可以用 dds_qos_dump() 函数打印当前配置,看看实际生效的参数是什么。这个函数在调试阶段特别有用。

好了,Cyclone DDS的入门就到这里。你动手试试,有问题随时翻回来看。记住,DDS的核心就是"数据为中心",QoS就是告诉中间件"怎么传数据"。搞懂这两点,后面ROS 2的通信机制你就掌握了一半。

可可精品资料库