Fast DDS 入门:简单应用程序
一、概述
本文档展示了如何使用 eProsima Fast DDS 库创建一个简单的发布-订阅应用程序。该应用程序由两个独立的程序组成:一个发布者和一个订阅者。发布者将发送一系列“Hello World”消息,订阅者将接收这些消息并打印出来。
通过本教程,您将学习如何:
- 定义用于 DDS 通信的数据类型
- 创建发布者应用程序
- 创建订阅者应用程序
- 编译和运行应用程序
二、前提条件
在开始之前,请确保您已按照安装指南安装了 eProsima Fast DDS 及其所有依赖项,以及 eProsima Fast DDS-Gen 工具。本教程中的所有命令均针对 Linux 环境。
三、创建应用程序工作区
项目完成后,应用程序工作区的结构如下。build/DDSHelloWorldPublisher 和 build/DDSHelloWorldSubscriber 文件分别是发布者应用程序和订阅者应用程序。
.
└── workspace_DDSHelloWorld
├── build
│ ├── CMakeCache.txt
│ ├── CMakeFiles
│ ├── cmake_install.cmake
│ ├── DDSHelloWorldPublisher
│ ├── DDSHelloWorldSubscriber
│ └── Makefile
├── CMakeLists.txt
└── src
├── HelloWorld.hpp
├── HelloWorld.idl
├── HelloWorldCdrAux.hpp
├── HelloWorldCdrAux.ipp
├── HelloWorldPublisher.cpp
├── HelloWorldPubSubTypes.cxx
├── HelloWorldPubSubTypes.h
├── HelloWorldSubscriber.cpp
├── HelloWorldTypeObjectSupport.cxx
└── HelloWorldTypeObjectSupport.hpp
首先创建目录结构:
mkdir workspace_DDSHelloWorld && cd workspace_DDSHelloWorld
mkdir src build
四、导入库及其依赖项
DDS 应用程序需要 Fast DDS 和 Fast CDR 库。根据您所遵循的安装过程,使这些库可用于 DDS 应用程序的方式略有不同。
4.1 从二进制文件安装和手动安装
如果您遵循了从二进制文件安装或手动安装的方式,这些库已经可以从工作区访问。在 Linux 上,Fast DDS 和 Fast CDR 的头文件分别位于 /usr/include/fastdds/ 和 /usr/include/fastcdr/ 目录中。两者的编译库都位于 /usr/lib/ 目录中。
4.2 Colcon 安装
从 Colcon 安装中,有几种导入库的方法。如果只需要在当前会话中使用这些库,请运行以下命令:
source <path/to/Fast-DDS/workspace>/install/setup.bash
通过在当前用户的 shell 配置文件中运行以下命令,将 Fast DDS 安装目录添加到 $PATH 变量中,可以使它们在任何会话中都可访问:
echo 'source <path/to/Fast-DDS/workspace>/install/setup.bash' >> ~/.bashrc
这将在该用户每次登录后设置环境。
五、配置 CMake 项目
我们将使用 CMake 工具来管理项目的构建。使用您喜欢的文本编辑器,创建一个名为 CMakeLists.txt 的新文件,并复制粘贴以下代码片段。将此文件保存在工作区的根目录中(如果按照这些步骤操作,应为 workspace_DDSHelloWorld)。
cmake_minimum_required(VERSION 3.20)
project(DDSHelloWorld)
# 查找依赖项
if(NOT fastcdr_FOUND)
find_package(fastcdr 2 REQUIRED)
endif()
if(NOT fastdds_FOUND)
find_package(fastdds 3 REQUIRED)
endif()
# 设置 C++11
include(CheckCXXCompilerFlag)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_COMPILER_IS_CLANG OR
CMAKE_CXX_COMPILER_ID MATCHES "Clang")
check_cxx_compiler_flag(-std=c++11 SUPPORTS_CXX11)
if(SUPPORTS_CXX11)
add_compile_options(-std=c++11)
else()
message(FATAL_ERROR "Compiler doesn't support C++11")
endif()
endif()
message(STATUS "Configuring HelloWorld publisher/subscriber example...")
file(GLOB DDS_HELLOWORLD_SOURCES_CXX "src/*.cxx")
# 发布者
add_executable(DDSHelloWorldPublisher
src/HelloWorldPublisher.cpp
${DDS_HELLOWORLD_SOURCES_CXX})
target_link_libraries(DDSHelloWorldPublisher
fastcdr
fastdds
pthread)
# 订阅者
add_executable(DDSHelloWorldSubscriber
src/HelloWorldSubscriber.cpp
${DDS_HELLOWORLD_SOURCES_CXX})
target_link_libraries(DDSHelloWorldSubscriber
fastcdr
fastdds
pthread)
六、构建主题数据类型
eProsima Fast DDS-Gen 是一个 Java 应用程序,它使用接口描述语言(IDL)文件中定义的数据类型生成源代码。此应用程序可以执行两项不同的操作:
- 为自定义主题生成 C++ 定义
- 生成使用主题数据的功能示例
本教程将采用前者。要查看后者的应用示例,可以查看另一个示例(详情参见引言)。对于此项目,我们将使用 Fast DDS-Gen 应用程序来定义将由发布者发送并由订阅者接收的消息的数据类型。
在工作区目录中,执行以下命令:
cd src && touch HelloWorld.idl
这会在 src 目录中创建 HelloWorld.idl 文件。在文本编辑器中打开该文件,并复制粘贴以下代码片段:
struct HelloWorld
{
unsigned long index;
string message;
};
通过这样做,我们定义了 HelloWorld 数据类型,它有两个元素:一个 uint32_t 类型的索引和一个 std::string 类型的消息。剩下的就是生成在 C++11 中实现此数据类型的源代码。为此,从 src 目录运行以下命令:
<path/to/Fast DDS-Gen>/scripts/fastddsgen HelloWorld.idl
这必须生成以下文件:
HelloWorld.hpp:HelloWorld 类型定义HelloWorldPubSubTypes.cxx:Fast DDS 用于支持 HelloWorld 类型的接口HelloWorldPubSubTypes.h:HelloWorldPubSubTypes.cxx的头文件HelloWorldCdrAux.ipp:HelloWorld 类型的序列化和反序列化代码HelloWorldCdrAux.hpp:HelloWorldCdrAux.ipp的头文件HelloWorldTypeObjectSupport.cxx:TypeObject 表示注册代码HelloWorldTypeObjectSupport.hpp:HelloWorldTypeObjectSupport.cxx的头文件
七、编写 Fast DDS 发布者
从工作区的 src 目录中,运行以下命令下载 HelloWorldPublisher.cpp 文件:
wget -O HelloWorldPublisher.cpp \
https://raw.githubusercontent.com/eProsima/Fast-RTPS-docs/master/code/Examples/C++/DDSHelloWorld/src/HelloWorldPublisher.cpp
这是发布者应用程序的 C++ 源代码。它将在 HelloWorldTopic 主题下发送 10 条发布消息。
// Copyright 2016 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @file HelloWorldPublisher.cpp
*
*/
#include "HelloWorldPubSubTypes.hpp"
#include <chrono>
#include <thread>
#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/publisher/DataWriterListener.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/topic/TypeSupport.hpp>
using namespace eprosima::fastdds::dds;
class HelloWorldPublisher
{
private:
HelloWorld hello_;
DomainParticipant* participant_;
Publisher* publisher_;
Topic* topic_;
DataWriter* writer_;
TypeSupport type_;
class PubListener : public DataWriterListener
{
public:
PubListener()
: matched_(0)
{
}
~PubListener() override
{
}
void on_publication_matched(
DataWriter*,
const PublicationMatchedStatus& info) override
{
if (info.current_count_change == 1)
{
matched_ = info.total_count;
std::cout << "Publisher matched." << std::endl;
}
else if (info.current_count_change == -1)
{
matched_ = info.total_count;
std::cout << "Publisher unmatched." << std::endl;
}
else
{
std::cout << info.current_count_change
<< " is not a valid value for PublicationMatchedStatus current count change." << std::endl;
}
}
std::atomic_int matched_;
} listener_;
public:
HelloWorldPublisher()
: participant_(nullptr)
, publisher_(nullptr)
, topic_(nullptr)
, writer_(nullptr)
, type_(new HelloWorldPubSubType())
{
}
virtual ~HelloWorldPublisher()
{
if (writer_ != nullptr)
{
publisher_->delete_datawriter(writer_);
}
if (publisher_ != nullptr)
{
participant_->delete_publisher(publisher_);
}
if (topic_ != nullptr)
{
participant_->delete_topic(topic_);
}
DomainParticipantFactory::get_instance()->delete_participant(participant_);
}
//! 初始化发布者
bool init()
{
hello_.index(0);
hello_.message("HelloWorld");
DomainParticipantQos participantQos;
participantQos.name("Participant_publisher");
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, participantQos);
if (participant_ == nullptr)
{
return false;
}
// 注册类型
type_.register_type(participant_);
// 创建发布主题
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (topic_ == nullptr)
{
return false;
}
// 创建发布者
publisher_ = participant_->create_publisher(PUBLISHER_QOS_DEFAULT, nullptr);
if (publisher_ == nullptr)
{
return false;
}
// 创建数据写入器
writer_ = publisher_->create_datawriter(topic_, DATAWRITER_QOS_DEFAULT, &listener_);
if (writer_ == nullptr)
{
return false;
}
return true;
}
//! 发送发布消息
bool publish()
{
if (listener_.matched_ > 0)
{
hello_.index(hello_.index() + 1);
writer_->write(&hello_);
return true;
}
return false;
}
//! 运行发布者
void run(
uint32_t samples)
{
uint32_t samples_sent = 0;
while (samples_sent < samples)
{
if (publish())
{
samples_sent++;
std::cout << "Message: " << hello_.message() << " with index: " << hello_.index()
<< " SENT" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
};
int main(
int argc,
char**argv)
{
std::cout << "Starting publisher." << std::endl;
uint32_t samples = 10;
HelloWorldPublisher* mypub = new HelloWorldPublisher();
if(mypub->init())
{
mypub->run(samples);
}
delete mypub;
return 0;
}
7.1 代码解析
代码开头是一个 Doxygen 风格的注释块,其中 @file 字段指明了文件名。
接下来是 C++ 头文件的包含。第一个包含的是 HelloWorldPubSubTypes.h 文件,该文件包含了我们在上一节中定义的数据类型的序列化和反序列化函数。
#include "HelloWorldPubSubTypes.hpp"
下一个代码块包含了允许使用 Fast DDS API 的 C++ 头文件:
DomainParticipantFactory:允许创建和销毁DomainParticipant对象DomainParticipant:作为所有其他实体对象的容器,并作为Publisher、Subscriber和Topic对象的工厂TypeSupport:为参与者提供序列化、反序列化和获取特定数据类型键的函数Publisher:负责创建DataWriters的对象DataWriter:允许应用程序设置要在给定主题下发布的数据的值DataWriterListener:允许重新定义DataWriterListener的函数
#include <chrono>
#include <thread>
#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/publisher/DataWriter.hpp>
#include <fastdds/dds/publisher/DataWriterListener.hpp>
#include <fastdds/dds/publisher/Publisher.hpp>
#include <fastdds/dds/topic/TypeSupport.hpp>
接下来,我们定义了包含将在应用程序中使用的 eProsima Fast DDS 类和函数的命名空间:
using namespace eprosima::fastdds::dds;
下一行创建了实现发布者的 HelloWorldPublisher 类:
class HelloWorldPublisher
查看类的私有数据成员,hello_ 数据成员被定义为 HelloWorld 类的对象,该类定义了我们用 IDL 文件创建的数据类型。接下来,定义了对应于参与者、发布者、主题、DataWriter 和数据类型的私有数据成员。TypeSupport 类的 type_ 对象是将用于在 DomainParticipant 中注册主题数据类型的对象。
private:
HelloWorld hello_;
DomainParticipant* participant_;
Publisher* publisher_;
Topic* topic_;
DataWriter* writer_;
TypeSupport type_;
然后,通过继承 DataWriterListener 类定义了 PubListener 类。此类重写默认的 DataWriter 监听器回调,这允许在发生事件时执行程序。重写的回调 on_publication_matched() 允许在检测到有新的 DataReader 正在监听 DataWriter 所发布的主题时定义一系列操作。info.current_count_change 检测与 DataWriter 匹配的 DataReaders 的这些变化。这是 MatchedStatus 结构中的一个成员,允许跟踪订阅状态的变化。最后,将该类的 listener_ 对象定义为 PubListener 的实例。
class PubListener : public DataWriterListener
{
public:
PubListener()
: matched_(0)
{
}
~PubListener() override
{
}
void on_publication_matched(
DataWriter*,
const PublicationMatchedStatus& info) override
{
if (info.current_count_change == 1)
{
matched_ = info.total_count;
std::cout << "Publisher matched." << std::endl;
}
else if (info.current_count_change == -1)
{
matched_ = info.total_count;
std::cout << "Publisher unmatched." << std::endl;
}
else
{
std::cout << info.current_count_change
<< " is not a valid value for PublicationMatchedStatus current count change." << std::endl;
}
}
std::atomic_int matched_;
} listener_;
HelloWorldPublisher 类的公共部分首先是构造函数和析构函数。构造函数初始化所有指针为 nullptr,并创建 TypeSupport 对象。析构函数按与创建相反的顺序销毁所有 DDS 实体,以避免内存泄漏。
public:
HelloWorldPublisher()
: participant_(nullptr)
, publisher_(nullptr)
, topic_(nullptr)
, writer_(nullptr)
, type_(new HelloWorldPubSubType())
{
}
virtual ~HelloWorldPublisher()
{
if (writer_ != nullptr)
{
publisher_->delete_datawriter(writer_);
}
if (publisher_ != nullptr)
{
participant_->delete_publisher(publisher_);
}
if (topic_ != nullptr)
{
participant_->delete_topic(topic_);
}
DomainParticipantFactory::get_instance()->delete_participant(participant_);
}
init() 方法负责初始化所有 DDS 实体:
- 初始化
hello_对象的字段。 - 创建域参与者(使用默认 QoS,域 ID 为 0)。
- 注册数据类型。
- 创建主题(名称为“HelloWorldTopic”,类型为“HelloWorld”)。
- 创建发布者(使用默认 QoS)。
- 创建数据写入器(使用默认 QoS,并绑定监听器)。
如果所有步骤都成功,则返回 true。
//! 初始化发布者
bool init()
{
hello_.index(0);
hello_.message("HelloWorld");
DomainParticipantQos participantQos;
participantQos.name("Participant_publisher");
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, participantQos);
if (participant_ == nullptr)
{
return false;
}
// 注册类型
type_.register_type(participant_);
// 创建发布主题
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (topic_ == nullptr)
{
return false;
}
// 创建发布者
publisher_ = participant_->create_publisher(PUBLISHER_QOS_DEFAULT, nullptr);
if (publisher_ == nullptr)
{
return false;
}
// 创建数据写入器
writer_ = publisher_->create_datawriter(topic_, DATAWRITER_QOS_DEFAULT, &listener_);
if (writer_ == nullptr)
{
return false;
}
return true;
}
publish() 方法检查是否有匹配的订阅者,如果有,则递增消息索引并发布消息。
//! 发送发布消息
bool publish()
{
if (listener_.matched_ > 0)
{
hello_.index(hello_.index() + 1);
writer_->write(&hello_);
return true;
}
return false;
}
run() 方法循环发布指定数量的消息(默认 10 条),每秒发布一条。
//! 运行发布者
void run(
uint32_t samples)
{
uint32_t samples_sent = 0;
while (samples_sent < samples)
{
if (publish())
{
samples_sent++;
std::cout << "Message: " << hello_.message() << " with index: " << hello_.index()
<< " SENT" << std::endl;
}
std::this_thread::sleep_for(std::chrono::milliseconds(1000));
}
}
main() 函数创建发布者实例,初始化并运行它,最后清理资源。
int main(
int argc,
char**argv)
{
std::cout << "Starting publisher." << std::endl;
uint32_t samples = 10;
HelloWorldPublisher* mypub = new HelloWorldPublisher();
if(mypub->init())
{
mypub->run(samples);
}
delete mypub;
return 0;
}
八、编写 Fast DDS 订阅者
从工作区的 src 目录中,运行以下命令下载 HelloWorldSubscriber.cpp 文件:
wget -O HelloWorldSubscriber.cpp \
https://raw.githubusercontent.com/eProsima/Fast-RTPS-docs/master/code/Examples/C++/DDSHelloWorld/src/HelloWorldSubscriber.cpp
这是订阅者应用程序的 C++ 源代码,用于接收发布者在“HelloWorldTopic”主题下发布的消息。
// Copyright 2016 Proyectos y Sistemas de Mantenimiento SL (eProsima).
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
/**
* @file HelloWorldSubscriber.cpp
*
*/
#include "HelloWorldPubSubTypes.hpp"
#include <fastdds/dds/domain/DomainParticipant.hpp>
#include <fastdds/dds/domain/DomainParticipantFactory.hpp>
#include <fastdds/dds/subscriber/DataReader.hpp>
#include <fastdds/dds/subscriber/DataReaderListener.hpp>
#include <fastdds/dds/subscriber/Subscriber.hpp>
#include <fastdds/dds/topic/TypeSupport.hpp>
using namespace eprosima::fastdds::dds;
class HelloWorldSubscriber
{
private:
HelloWorld hello_;
DomainParticipant* participant_;
Subscriber* subscriber_;
Topic* topic_;
DataReader* reader_;
TypeSupport type_;
class SubListener : public DataReaderListener
{
public:
SubListener()
: matched_(0)
, samples_(0)
{
}
~SubListener() override
{
}
void on_data_available(
DataReader* reader) override
{
SampleInfo info;
if (reader->take_next_sample(&hello_, &info) == RETCODE_OK)
{
if (info.valid_data)
{
samples_++;
std::cout << "Message: " << hello_.message() << " with index: " << hello_.index()
<< " RECEIVED. Total samples: " << samples_ << std::endl;
}
}
}
void on_subscription_matched(
DataReader*,
const SubscriptionMatchedStatus& info) override
{
if (info.current_count_change == 1)
{
matched_ = info.total_count;
std::cout << "Subscriber matched." << std::endl;
}
else if (info.current_count_change == -1)
{
matched_ = info.total_count;
std::cout << "Subscriber unmatched." << std::endl;
}
else
{
std::cout << info.current_count_change
<< " is not a valid value for SubscriptionMatchedStatus current count change." << std::endl;
}
}
HelloWorld hello_;
std::atomic_int matched_;
uint32_t samples_;
} listener_;
public:
HelloWorldSubscriber()
: participant_(nullptr)
, subscriber_(nullptr)
, topic_(nullptr)
, reader_(nullptr)
, type_(new HelloWorldPubSubType())
{
}
virtual ~HelloWorldSubscriber()
{
if (reader_ != nullptr)
{
subscriber_->delete_datareader(reader_);
}
if (subscriber_ != nullptr)
{
participant_->delete_subscriber(subscriber_);
}
if (topic_ != nullptr)
{
participant_->delete_topic(topic_);
}
DomainParticipantFactory::get_instance()->delete_participant(participant_);
}
//! 初始化订阅者
bool init()
{
DomainParticipantQos participantQos;
participantQos.name("Participant_subscriber");
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, participantQos);
if (participant_ == nullptr)
{
return false;
}
// 注册类型
type_.register_type(participant_);
// 创建主题
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (topic_ == nullptr)
{
return false;
}
// 创建订阅者
subscriber_ = participant_->create_subscriber(SUBSCRIBER_QOS_DEFAULT, nullptr);
if (subscriber_ == nullptr)
{
return false;
}
// 创建数据读取器
reader_ = subscriber_->create_datareader(topic_, DATAREADER_QOS_DEFAULT, &listener_);
if (reader_ == nullptr)
{
return false;
}
return true;
}
//! 运行订阅者
void run()
{
std::cout << "Waiting for messages... (Press Ctrl+C to exit)" << std::endl;
while (true)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
};
int main(
int argc,
char**argv)
{
std::cout << "Starting subscriber." << std::endl;
HelloWorldSubscriber* mysub = new HelloWorldSubscriber();
if(mysub->init())
{
mysub->run();
}
delete mysub;
return 0;
}
8.1 代码解析
订阅者代码结构与发布者类似,但使用了订阅者相关的 DDS 实体(Subscriber、DataReader、DataReaderListener)。
SubListener 类重写了两个关键回调:
on_data_available():当有新数据可用时被调用,通过take_next_sample()方法读取数据并打印。on_subscription_matched():当与发布者的匹配状态变化时被调用。
init() 方法初始化订阅者实体,流程与发布者类似,但创建的是订阅者和数据读取器。
run() 方法使订阅者进入无限循环,持续等待并接收消息。
九、编译和运行应用程序
9.1 编译
从工作区的 build 目录执行以下命令:
cd ../build
cmake ..
make
这将生成两个可执行文件:DDSHelloWorldPublisher 和 DDSHelloWorldSubscriber。
9.2 运行
-
启动订阅者(在一个终端中):
./DDSHelloWorldSubscriber输出:
Starting subscriber. Waiting for messages... (Press Ctrl+C to exit) -
启动发布者(在另一个终端中):
./DDSHelloWorldPublisher发布者输出:
Starting publisher. Publisher matched. Message: HelloWorld with index: 1 SENT Message: HelloWorld with index: 2 SENT ... Message: HelloWorld with index: 10 SENT -
订阅者输出:
Subscriber matched. Message: HelloWorld with index: 1 RECEIVED. Total samples: 1 Message: HelloWorld with index: 2 RECEIVED. Total samples: 2 ... Message: HelloWorld with index: 10 RECEIVED. Total samples: 10
十、后续步骤
完成本教程后,您可以进一步探索 Fast DDS 的更多功能:
- 自定义 QoS 策略:修改可靠性、持久性、截止期限等 QoS 参数,观察其对通信的影响。
- 使用 XML 配置:通过 XML 文件配置 DDS 实体,实现无需重新编译即可调整参数。
- 尝试复杂数据类型:定义包含数组、嵌套结构等的 IDL 类型,测试复杂数据的传输。
- 探索发现机制:了解 Fast DDS 的自动发现原理,配置静态发现或发现服务器模式。
- 性能测试:使用 Fast DDS 提供的工具测试不同配置下的吞吐量和延迟。
更多详细信息,请参考 Fast DDS 官方文档的高级特性部分。
Fast DDS快速入门与应用案例

1万+






