1 开始使用
1.1 DDS是什么?
数据分发服务(DDS)是用于分布式软件应用程序通信的数据中心通信协议。它描述了通信应用程序编程接口(API)和通信语义,以实现数据提供者和数据消费者之间的通信。
由于它是一个数据中心发布-订阅(DCPS)模型,在其实现中定义了三个关键应用实体:发布实体,用于定义信息生成对象及其属性;订阅实体,用于定义信息消费对象及其属性;以及配置实体,用于定义作为主题传输的信息类型,并使用其质量服务(QoS)属性创建发布者和订阅者,确保上述实体的正确性能。
DDS 使用 QoS 来定义 DDS 实体的行为特征。QoS 由单独的 QoS 策略(派生自 QoSPolicy 类型的对象)组成。这些策略在 Policy(文档3.1.2) 中进行了描述。
1.1.1 DCPS 概念模型
在 DCPS 模型中,定义了用于开发通信应用系统的四个基本元素:
-
Publisher。它是负责创建和配置其实现的 DataWriter 的 DCPS 实体。DataWriter 是负责实际发布消息的实体。每个 DataWriter 将被分配一个主题,用于发布消息。有关详细信息,请参阅发布者(文档3.3)。
-
Subscriber。它是负责接收其订阅的主题下发布的数据的 DCPS 实体。它为一个或多个 DataReader 对象提供服务,负责将新数据的可用性通知给应用程序。有关详细信息,请参阅订阅者(文档3.4)。
-
Topic。它是将发布和订阅绑定在一起的实体。在 DDS 域内是唯一的。通过 TopicDescription,它允许发布和订阅的数据类型的统一性。有关详细信息,请参阅主题(文档3.5)。
-
Domain。这是用于连接所有发布者和订阅者的概念,这些发布者和订阅者属于一个或多个应用程序,在不同的主题下交换数据。参与领域的这些个别应用程序被称为 DomainParticipant。DDS 领域由领域 ID 标识。DomainParticipant 定义了领域 ID 以指定其所属的 DDS 领域。具有不同 ID 的两个 DomainParticipant 在网络中不知道彼此的存在。因此,可以创建多个通信通道。这适用于涉及多个 DDS 应用程序的场景,其各自的 DomainParticipant 互相通信,但这些应用程序不能相互干扰。DomainParticipant 充当其他 DCPS 实体的容器,充当 Publisher、Subscriber 和 Topic 实体的工厂,并在领域中提供管理服务。有关详细信息,请参阅领域(文档3.2)。
这些元素如下图所示:
1.2 RTPS是什么?
为了支持DDS应用程序而开发的实时发布订阅(RTPS)协议,是一种基于尽力而为传输(如UDP/IP)的发布-订阅通信中间件。此外,Fast DDS 还支持 TCP 和共享内存(SHM)传输。
它旨在支持单播和组播通信。
在 RTPS 顶层,继承自 DDS,可以找到Domain(域),它定义了一个独立的通信平面。多个领域可以独立地同时共存。一个领域包含任意数量的 RTPSParticipants,即能够发送和接收数据的元素。为了实现这一点,RTPSParticipants 使用它们的端点:
-
RTPSWriter: 能够发送数据的端点。
-
RTPSReader: 能够接收数据的端点。
一个 RTPSParticipant 可以拥有任意数量的写入端点和读取端点。
通信围绕Topic(主题)展开,主题定义和标记正在交换的数据。主题不属于特定的参与者。参与者通过 RTPS 写入器对在主题下发布的数据进行更改,并通过 RTPS 读取器接收与其订阅的主题相关联的数据。通信单元称为Change(变更),它表示在主题下编写的数据的更新。RTPSReaders/RTPSWriters(RTPS 读取器/RTPS 写入器)在其历史记录上注册这些变更,历史记录是一种用作最近更改的缓存的数据结构。
在 eProsima Fast DDS 的默认配置中,当通过 RTPSWriter 端点发布更改时,后台会发生以下步骤:
-
更改被添加到 RTPSWriter 的历史缓存中。
-
RTPSWriter 将更改发送到它所知道的任何 RTPSReaders。
-
接收数据后,RTPSReaders 使用新的更改更新其历史缓存。
然而,Fast DDS 支持多种配置,允许您更改 RTPS 实体的行为。对 RTPS 实体默认配置的修改意味着改变 RTPSWriters 和 RTPSReaders 之间的数据交换流程。此外,通过选择服务质量(QoS)策略,您可以以多种方式影响这些历史缓存的管理,但通信循环保持不变。您可以继续阅读 RTPS 层部分(文档4),以了解有关 Fast DDS 中 RTPS 协议实现的更多信息。
1.3 编写一个简单的C++发布订阅程序
这一节详细介绍怎么用C++ API一步一步的创建一个简单的FastDDS应用程序,包含发布、订阅。你也可以用eProsima FastDDS Gen工具自行生成与这节中类似的代码。
文档版本:v2.13.0
1.3.1 背景
DDS是一种以数据为中心的通信中间件,实现了DCPS模型。该模型基于发布者(数据生成元素)和订阅者(数据消费元素)的开发。这些实体通过Topic(主题)进行通信,Topic是将DDS实体绑定在一起的元素。Publishers在某个Topic下生成信息,而Subscribers订阅相同的Topic以接收信息。
1.3.2 前提
首先,您需要按照 eProsima Fast DDS 安装手册中概述的步骤安装 eProsima Fast DDS 及其所有依赖项。您还需要完成 eProsima Fast DDS-Gen 工具安装手册中概述的步骤。此外,本教程中提供的所有命令均适用于 Linux 环境。
1.3.3 创建应用程序工作空间
在项目结束时,应用程序工作空间将具有以下结构:文件 build/``DDSHelloWorldPublisher
和 build/``DDSHelloWorldSubscriber
分别是发布者、订阅者应用程序。
先创建目录树:
1.3.4 导入链接库和依赖项
DDS应用程序需要Fast DDS和Fast CDR库。根据所遵循的安装程序,使这些库对我们的DDS应用程序可用的过程会略有不同。
1.3.4.1 从二进制文件安装和手动安装
如果我们按照二进制文件安装或手动安装的步骤,这些库已经可以从工作空间中访问。在Linux上,Fast DDS和Fast CDR的头文件分别可以在目录/usr/include/fastrtps/
和/usr/include/fastcdr/
中找到。两者的编译库可以在目录/usr/lib/中找到。
1.3.4.2 Colcon安装
在进行Colcon安装后,有几种方法可以导入这些库。如果这些库只需要在当前会话中可用,运行以下命令。
可以通过将Fast DDS安装目录添加到当前用户的shell配置文件中的$PATH
变量,使其在任何会话中可访问,方法是运行以下命令。
这将在用户每次登录后设置环境。
1.3.5 配置CMake工程
我们将使用CMake工具来管理项目的构建。使用您喜欢的文本编辑器,创建一个名为CMakeLists.txt的新文件,复制并粘贴以下代码片段。将此文件保存在工作空间的根目录中。如果您按照这些步骤操作,根目录应该是workspace_DDSHelloWorld
。
在每一节中,我们将完成此文件,用以包含指定的生成文件。
1.3.6 构建topic数据类型
eProsima Fast DDS-Gen是一个Java应用程序,它使用接口描述语言(IDL)文件中定义的数据类型生成源代码。该应用程序可以执行两种不同的操作:
-
为您的自定义主题生成C++定义。
-
生成使用您的主题数据的功能示例。
在本教程中,我们将遵循第一种操作。要查看后者的应用示例,您可以查看另一个示例。有关更多详细信息,请参阅介绍。对于本项目,我们将使用Fast DDS-Gen应用程序来定义由发布者发送和订阅者接收的消息的数据类型。
在工作空间目录中,执行以下命令:
这将在src目录中创建HelloWorld.idl文件。在文本编辑器中打开该文件,并复制粘贴以下代码片段。
通过这样做,我们已经定义了HelloWorld数据类型,它有两个元素:uint32_t index
、std::string message
。现在只剩下生成实现这种数据类型的C++11源代码了。要做到这一点,从src目录运行以下命令。
这个命令应该生成了以下文件:
- HelloWorld.cxx:HelloWorld类型定义。
- HelloWorld.h:
HelloWorld.cxx的头文件。 - HelloWorldPubSubTypes.cxx:Fast DDS用于支持HelloWorld类型的接口。
- HelloWorldPubSubTypes.h:
HelloWorldPubSubTypes.cxx的头文件。 - HelloWorldCdrAux.ipp:HelloWorld类型的序列化和反序列化代码。
- HelloWorldCdrAux.hpp:HelloWorldCdrAux.ipp的头文件。
1.3.6.1 CMakeLists.txt
在您之前创建的CMakeList.txt文件的末尾包含以下代码片段。这将包括我们刚刚创建的文件。
1.3.7 编写Fast DDS publisher
从工作空间的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.h"
#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_);
}
//!Initialize the publisher
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;
}
// Register the Type
type_.register_type(participant_);
// Create the publications Topic
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (topic_ == nullptr)
{
return false;
}
// Create the Publisher
publisher_ = participant_->create_publisher(PUBLISHER_QOS_DEFAULT, nullptr);
if (publisher_ == nullptr)
{
return false;
}
// Create the DataWriter
writer_ = publisher_->create_datawriter(topic_, DATAWRITER_QOS_DEFAULT, &listener_);
if (writer_ == nullptr)
{
return false;
}
return true;
}
//!Send a publication
bool publish()
{
if (listener_.matched_ > 0)
{
hello_.index(hello_.index() + 1);
writer_->write(&hello_);
return true;
}
return false;
}
//!Run the Publisher
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;
}
1.3.7.1 解读代码
在文件开头,我们有一个Doxygen风格的注释块,其中包含@file字段,告诉我们文件的名称。
以下是C++头文件的包含部分。第一个包含了HelloWorldPubSubTypes.h
文件,其中包含了我们在前一节中定义的数据类型的序列化和反序列化函数。
下面这一块包括了允许使用Fast DDS API的C++头文件。
DomainParticipantFactory
。允许创建和销毁DomainParticipant对象。DomainParticipant
。作为所有其他实体对象的容器,同时也是Publisher、Subscriber和Topic对象的工厂。TypeSupport
。为参与者提供了序列化、反序列化和获取特定数据类型键的函数。Publisher
。负责创建DataWriter对象。DataWriter
。允许应用程序设置要在给定主题下发布的数据的值。DataWriterListener
。允许重新定义DataWriterListener的函数。
接下来,我们定义包含eProsima Fast DDS类和函数的命名空间,这些类和函数将在我们的应用程序中使用。
下一行创建了HelloWorldPublisher
类,该类实现了一个发布者。
在类的私有数据成员方面,hello_
数据成员被定义为HelloWorld
类的对象,该类定义了我们使用IDL文件创建的数据类型。接着,定义了与参与者、发布者、主题、DataWriter、数据类型相对应的私有数据成员。TypeSupport
类的type_
对象将用于在DomainParticipant中注册主题数据类型。
然后,通过继承DataWriterListener
类来定义PubListener
类。这个类重写了默认的DataWriter监听器回调,允许在发生事件时执行例程。重写的on_publication_matched()
回调允许在检测到新的DataReader监听DataWriter正在发布的主题时定义一系列操作。info.current_count_change()
检测与DataWriter匹配的DataReader的这些变化。这是MatchedStatus
结构中的一个成员,允许跟踪订阅状态的变化。最后,类的listener_
对象被定义为PubListener
的一个实例。
HelloWorldPublisher
类的公共构造函数和析构函数如下。构造函数将类的私有数据成员初始化为nullptr
,但TypeSupport对象除外,它被初始化为HelloWorldPubSubType类的实例。类的析构函数会移除这些数据成员,从而清理系统内存。
HelloWorldPublisher
类的公共成员函数继续如下定义。下面的代码片段定义了公共发布者初始化成员函数。该函数执行以下几项操作:
-
初始化HelloWorld类型
hello_
结构成员的内容。 -
通过DomainParticipant的QoS为参与者分配一个名称。
-
使用
DomainParticipantFactory
创建参与者。 -
注册在IDL中定义的数据类型。
-
创建用于发布的主题。
-
创建发布者。
-
使用先前创建的监听器创建DataWriter。
除了参与者的名称之外,所有实体的QoS配置都是默认配置(PARTICIPANT_QOS_DEFAULT
,PUBLISHER_QOS_DEFAULT
,TOPIC_QOS_DEFAULT
,DATAWRITER_QOS_DEFAULT
)。每个DDS实体的QoS的默认值可以在DDS标准中查到。
为了进行发布,实现了公共成员函数publish()
。在DataWriter的监听器回调中,它声明DataWriter已经与监听发布主题的DataReader匹配,数据成员matched_
被更新。它包含了发现的DataReader的数量。因此,当发现第一个DataReader时,应用程序开始发布。这只是通过DataWriter对象写入一个变化。
公共run函数执行给定次数的发布操作,在每次发布之间等待1秒。
最后,在主函数中初始化并运行HelloWorldPublisher。
1.3.7.2 CMakeList.txt
在之前创建的CMakeList.txt文件的末尾包含以下代码片段。这将添加构建可执行文件所需的所有源文件,并将可执行文件与库链接在一起。
此时,项目已准备好进行构建、编译和运行发布者应用程序。在工作空间的构建目录中,运行以下命令。
1.3.8 编写Fast DDS subscriber
从工作空间中的src目录执行以下命令来下载HelloWorldSubscriber.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 HelloWorldSubscriber.cpp
*
*/
#include "HelloWorldPubSubTypes.h"
#include <chrono>
#include <thread>
#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/qos/DataReaderQos.hpp>
#include <fastdds/dds/subscriber/SampleInfo.hpp>
#include <fastdds/dds/subscriber/Subscriber.hpp>
#include <fastdds/dds/topic/TypeSupport.hpp>
using namespace eprosima::fastdds::dds;
class HelloWorldSubscriber
{
private:
DomainParticipant* participant_;
Subscriber* subscriber_;
DataReader* reader_;
Topic* topic_;
TypeSupport type_;
class SubListener : public DataReaderListener
{
public:
SubListener()
: samples_(0)
{
}
~SubListener() override
{
}
void on_subscription_matched(
DataReader*,
const SubscriptionMatchedStatus& info) override
{
if (info.current_count_change == 1)
{
std::cout << "Subscriber matched." << std::endl;
}
else if (info.current_count_change == -1)
{
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;
}
}
void on_data_available(
DataReader* reader) override
{
SampleInfo info;
if (reader->take_next_sample(&hello_, &info) == ReturnCode_t::RETCODE_OK)
{
if (info.valid_data)
{
samples_++;
std::cout << "Message: " << hello_.message() << " with index: " << hello_.index()
<< " RECEIVED." << std::endl;
}
}
}
HelloWorld hello_;
std::atomic_int 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 (topic_ != nullptr)
{
participant_->delete_topic(topic_);
}
if (subscriber_ != nullptr)
{
participant_->delete_subscriber(subscriber_);
}
DomainParticipantFactory::get_instance()->delete_participant(participant_);
}
//!Initialize the subscribe
bool init()
{
DomainParticipantQos participantQos;
participantQos.name("Participant_subscriber");
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, participantQos);
if (participant_ == nullptr)
{
return false;
}
// Register the Type
type_.register_type(participant_);
// Create the subscriptions Topic
topic_ = participant_->create_topic("HelloWorldTopic", "HelloWorld", TOPIC_QOS_DEFAULT);
if (topic_ == nullptr)
{
return false;
}
// Create the Subscriber
subscriber_ = participant_->create_subscriber(SUBSCRIBER_QOS_DEFAULT, nullptr);
if (subscriber_ == nullptr)
{
return false;
}
// Create the DataReader
reader_ = subscriber_->create_datareader(topic_, DATAREADER_QOS_DEFAULT, &listener_);
if (reader_ == nullptr)
{
return false;
}
return true;
}
//!Run the Subscriber
void run(
uint32_t samples)
{
while(listener_.samples_ < samples)
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
}
}
};
int main(
int argc,
char** argv)
{
std::cout << "Starting subscriber." << std::endl;
uint32_t samples = 10;
HelloWorldSubscriber* mysub = new HelloWorldSubscriber();
if(mysub->init())
{
mysub->run(samples);
}
delete mysub;
return 0;
}
1.3.8.1 解读代码
由于发布者和订阅者应用程序的源代码大部分相同,因此本文档将重点介绍它们之间的主要区别,省略了已经解释过的代码部分。
在订阅者说明中,遵循与发布者相同的结构,第一步是包含C++头文件。在这些文件中,包含发布者类的文件被订阅者类替换,数据写入器类被数据读取器类替换。
Subscriber
。这是负责创建和配置数据读取器的对象。DataReader
。这是负责实际接收数据的对象。它在应用程序中注册标识要读取的数据的主题(TopicDescription),并访问订阅者接收到的数据。DataReaderListener
。数据读取器监听器。这是分配给数据读取器的监听器。DataReaderQoS
。定义数据读取器的QoS的结构。SampleInfo
。这是每个“读取”或“取出”的样本的信息。
接下来的一行定义了HelloWorldSubscriber
类,它实现了一个订阅者。
从类的私有数据成员开始,值得一提的是数据读取器监听器的实现。类的私有数据成员将包括参与者、订阅者、主题、数据读取器和数据类型。与数据写入器一样,监听器实现了在发生事件时执行的回调。SubListener的第一个重写的回调是on_subscription_matched()
,它是DataWriter的on_publication_matched()
回调的类似物。
第二个重写的回调是on_data_available()
。在这个回调中,数据读取器可以访问并处理下一个接收到的样本,以显示其内容。在这里,定义了SampleInfo
类的对象,用于确定样本是否已经被读取或获取。每次读取一个样本时,接收到的样本计数器都会增加。
以下是该类的公共构造函数和析构函数。
接下来是订阅者初始化的公共成员函数。这与为HelloWorldPublisher
定义的初始化公共成员函数相同。除了参与者的名称之外,所有实体的QoS配置都是默认QoS(PARTICIPANT_QOS_DEFAULT
,SUBSCRIBER_QOS_DEFAULT
,TOPIC_QOS_DEFAULT
,DATAREADER_QOS_DEFAULT
)。每个DDS实体的QoS的默认值可以在DDS标准中查到。
公共成员函数run()确保订阅者一直运行,直到接收到所有样本。该成员函数实现了订阅者的主动等待,每隔100毫秒进行一次休眠以减轻CPU负担。
最后,在主函数中初始化并运行实现订阅者的参与者。
1.3.8.2 CMakeLists.txt
在之前创建的CMakeList.txt文件的末尾包含以下代码片段。这将添加构建可执行文件所需的所有源文件,并将可执行文件和库链接在一起。
此时,项目已准备好进行构建、编译和运行订阅者应用程序。在工作空间的构建目录中,运行以下命令。
1.3.9 整合
最后,从构建目录中,在两个终端中分别运行发布者和订阅者应用程序。
1.3.10 总结
在本教程中,你学会了:
- 你已经构建了一个发布者和一个订阅者DDS应用程序。
- 你还学会了如何构建用于源代码编译的CMake文件,以及如何在项目中包含和使用Fast DDS和Fast CDR库。
1.3.11 下一步
在eProsima Fast DDS的Github存储库中,你还能找到更复杂的示例,用于实现DDS通信以满足多种用例和场景。
Fast-DDS远程仓库地址:github.com/eProsima/Fa…
2 库概述
Fast DDS(原名 Fast RTPS)是DDS规范的高效高性能实现,是面向数据的通信中间件(DCPS),用于分布式应用软件。本节将回顾 Fast DDS 的架构、运行方式和关键特性。
2.1 架构
Fast DDS 的架构如下图所示,其中可以看到一个具有不同环境的分层模型。
- Application layer(应用层)。用户应用程序利用 Fast DDS API 实现在分布式系统中的通信。
- Fast DDS 层。DDS 通信中间件的强大实现。它允许部署一个或多个 DDS 领域,在同一领域内的 DomainParticipants 通过在领域主题下发布/订阅来交换消息。
- RTPS 层。实现实时发布-订阅(RTPS)协议,以实现与DDS应用程序的互操作性。该层充当传输层的抽象层。
- Transport Layer(传输层)。Fast DDS 可以在各种传输协议上使用,如不可靠传输协议(UDP)、可靠传输协议(TCP)或共享内存传输协议(SHM)。
2.1.1 DDS层
在 Fast DDS 的 DDS 层中定义了用于通信的几个关键元素。用户将在其应用程序中创建这些元素,从而将 DDS 应用程序元素整合到数据中心通信系统中。Fast DDS 遵循 DDS 规范,将参与通信的这些元素定义为实体。DDS 实体是支持服务质量配置(QoS)并实现监听器的任何对象。
-
QoS。定义每个实体行为的机制。
-
Listener(监听器)。通知实体可能在应用程序执行期间发生的事件的机制。
以下列出了 DDS 实体及其描述和功能。有关每个实体、它们的 QoS 和监听器的更详细解释,请参阅 DDS 层部分(文档3)。
- Domain(域)。用于标识 DDS 域的正整数。每个 DomainParticipant 将分配一个 DDS 域,以便在同一域中的 DomainParticipants 可以进行通信,并且在 DDS 域之间隔离通信。在创建 DomainParticipants 时,应用程序开发人员必须提供此值。
- DomainParticipant. 包含其他 DDS 实体(如发布者、订阅者、主题和多主题)的对象。它是允许创建它所包含的先前实体的实体,以及配置它们行为的实体。
- Publisher(发布者)。发布者使用数据写入器(DataWriter)在主题下发布数据,数据写入器将数据写入传输中。发布者是创建和配置其所包含的数据写入器实体的实体,并且可以包含一个或多个数据写入器。
- DataWriter(数据写入器)。它负责发布消息的实体。用户在创建此实体时必须提供一个主题,该主题将是数据发布的主题。发布是通过将数据对象作为数据写入器历史记录中的更改来完成的。
- DataWriterHistory(数据写入器历史记录)。这是数据对象更改的列表。当数据写入器开始在特定主题下发布数据时,它实际上创建了这些数据的更改。这些更改被记录在历史记录中。然后将这些更改发送到订阅该特定主题的数据读取器。
- Subscriber(订阅者)。订阅者使用数据读取器(DataReader)订阅主题,数据读取器从传输中读取数据。订阅者是创建和配置其所包含的数据读取器实体的实体,并且可以包含一个或多个数据读取器实体。
- DataReader(数据读取器)。它是订阅主题以接收发布的实体。用户在创建此实体时必须提供订阅主题。数据读取器将消息作为其历史数据读取器中的更改接收。
- DataReaderHistory(数据读取器历史记录)。它包含数据读取器订阅特定主题后接收到的数据对象更改。
- Topic(主题)。将发布者的数据写入器与订阅者的数据读取器绑定的实体。
2.1.2 RTPS层
如上所述,Fast DDS 中的 RTPS 协议允许将 DDS 应用程序实体与传输层进行抽象。根据上面显示的图表,RTPS 层有四个主要实体。
-
RTPSDomain. 它是将 DDS 域扩展到 RTPS 协议的实体。
-
RTPSParticipant. 包含其他 RTPS 实体的实体。它允许配置和创建其所包含的实体。
-
RTPSWriter. 消息的源头。它读取数据写入器历史记录中的更改,并将其传输给之前匹配的所有 RTPSReader。
-
RTPSReader. 消息的接收实体。它将由 RTPSWriter 报告的更改写入数据读取器历史记录。
要了解每个实体、它们的属性和它们的监听器的更详细解释,请参考 RTPS 层部分(文档6)。
2.1.3 传输层
Fast DDS 支持在各种传输协议上实现应用程序。这些协议包括 UDPv4、UDPv6、TCPv4、TCPv6 和共享内存传输(SHM)。默认情况下,DomainParticipant 实现了 UDPv4 和 SHM 传输协议。所有支持的传输协议的配置详细说明请参见传输层部分(文档6)。
2.2 编程和执行模型
Fast DDS 是并发和基于事件的。以下解释了管理 Fast DDS 操作的多线程模型以及可能的事件。
2.2.1 并发和多线程
Fast DDS 实现了一个并发的多线程系统。每个 DomainParticipant 生成一组线程来处理后台任务,如日志记录、消息接收和异步通信。这不应影响您使用库的方式,即 Fast DDS API 是线程安全的,因此您可以放心地从不同的线程中调用同一个 DomainParticipant 上的任何方法。然而,当外部函数访问由库内部运行的线程修改的资源时,必须考虑这种多线程实现。其中一个例子就是实体监听器回调中的修改资源。
Fast DDS 生成的完整线程集如下所示。与传输相关的线程(标记为 UDP、TCP 和 SHM 类型)仅在使用相应的传输时才会创建。
名称 | 类型 | 基数 | 操作系统线程名 | 描述 |
---|---|---|---|---|
Event | General | 每个DomainParticipant一个 | dds.ev.<participant_id> | 处理周期性和触发式时间事件。请参阅 DomainParticipantQos |
Discovery Server Event | General | 每个DomainParticipant一个 | dds.ds_ev.<participant_id> | 同步访问发现服务器数据库。请参阅 DomainParticipantQos。 |
Asynchronous Writer | General | 每个已启用的异步流控制器一个。最少1个。 | dds.asyn.<participant_id>. <async_flow_controller_index> | 管理异步写操作。即使对于同步写入器,某些形式的通信也必须在后台发起。请参阅 DomainParticipantQos 和 FlowControllersQos。 |
Datasharing Listener | General | 每个DataReader一个 | dds.dsha.<reader_id> | 处理通过数据共享接收的消息的监听器线程。请参阅 DataReaderQos。 |
Reception | UDP | 每个port一个 | dds.udp.<port> | 处理传入UDP消息的监听器线程。请参阅 TransportConfigQos 和 UDPTransportDescriptor。 |
Reception | TCP | 每个TCP连接一个 | dds.tcp.<port> | 处理传入TCP消息的监听器线程。请参阅 TCPTransportDescriptor。 |
Accept | TCP | 每个TCP传输一个 | dds.tcp_accept | 处理传入TCP连接请求的线程。请参阅 TCPTransportDescriptor。 |
Keep Alive | TCP | 每个TCP传输一个 | dds.tcp_keep | TCP连接的保持活动线程。请参阅 TCPTransportDescriptor。 |
Reception | SHM | 每个port一个 | dds.shm.<port> | 处理通过SHM段接收的消息的监听器线程。请参阅 TransportConfigQos 和 SharedMemTransportDescriptor。 |
其中一些线程只会在满足特定条件时才会生成:
-
数据共享监听器线程仅在使用数据共享时创建。
-
发现服务器事件线程仅在 DomainParticipant 配置为发现服务器 SERVER 时创建。
-
TCP keep alive 线程需要将保持活动周期配置为大于零的值。
-
安全日志记录和共享内存数据包记录线程都需要启用某些配置选项。
-
Filewatch 线程仅在使用 FASTDDS_ENVIRONMENT_FILE 时才会生成。
关于传输线程,默认情况下 Fast DDS 同时使用 UDP 和共享内存传输。端口配置可以根据部署的特定需求进行配置,但默认配置始终使用元流量端口和单播用户流量端口。这适用于 UDP 和共享内存,因为 TCP 不支持多播。有关更多信息,请参阅默认监听定位器页面。
Fast DDS 提供了通过 ThreadSettings 配置其创建的某些线程属性的可能性。
15 典型用例
15.12 如何使用eProsima DDS Record and Replay(rosbag2和DDS)
eProsima DDS Record and Replay 允许用户实时连续监控ROS 2通信,并随时进行回放。这对于模拟真实环境条件、应用测试、优化数据分析和一般故障排除大有裨益。rosbag2是一个ROS 2应用程序,可用于捕获DDS消息并将其存储在SQLite数据库中,从而可以在以后检查和重放这些消息。
15.12.1 rosbag2与本机Fast DDS应用程序的交互
使用rosbag2捕获ROS 2 talkers和listeners之间的通信非常简单。然而,记录和重放由ROS 2生态系统外的Fast DDS参与者发送的消息需要进行一些修改。
15.12.1.1 前提条件
需要安装Fast DDS,可以是二进制安装包,也可以是源代码安装。还需要安装Fast DDS-Gen以生成示例和从IDL文件生成Fast DDS TypeSupport。同时还需要安装ROS 2,并包含rosbag2软件包。
15.12.1.2 DDS IDL与ROS 2消息的互操作性
DDS使用IDL(接口定义语言)来定义应用程序之间交换的数据模型。虽然ROS 2可以使用IDL文件来定义消息,但这些IDL文件必须遵循一些规则,以便实现ROS 2与Fast DDS本机应用程序之间的兼容性。具体来说,类型定义必须嵌套在类型模块名称内,然后使用相应的生成器。对于ROS 2消息,生成器将是msg
,而在这种情况下,必须使用idl生成器。假设选择的类型模块名称是fastdds_record_typesupport
,则可以定义如下的HelloWorld.idl
文件。这个IDL文件将在接下来的步骤中使用。
默认情况下,rosbag2只能识别那些ROS 2在其不同的TypeSupport库中已经定义的类型的主题。因此,必须创建一个使用先前定义的类型生成的新的ROS 2 TypeSupport模块库,以便rosbag2能够解析来自Fast DDS应用程序的消息内容。首先,应该创建新的ROS 2 TypeSupport包。在设置了ROS 2安装环境变量后,按照以下说明操作:
这个命令将创建一个名为fastdds_record_typesupport
的新的ROS 2包,具有以下文件夹结构:
ROS 2 TypeSupport代码生成器需要将IDL文件放在它们自己的idl文件夹内,因此最终的文件夹结构将如下所示:
为了生成所需的TypeSupport接口,应相应地修改CMakeLists.txt文件,以便调用ROS 2 TypeSupport生成器。请在调用ament_package()
之前向CMakeLists.txt文件添加以下行:
类似地,应修改package.xml
文件,添加ROS 2 TypeSupport生成器的依赖项。请在buildtool_depend
标签后向package.xml
文件添加以下行:
最后一步是构建该包。在fastdds_record_typesupport
文件夹中运行以下命令:
构建过程将在install文件夹中创建一个新的ROS 2叠加层,其中包含所有所需的库和脚本,以便ROS 2应用程序可以使用IDL文件中定义的类型。
15.12.1.3 Fast DDS 应用程序调优
ROS 2根据主题所属的ROS 2子系统向主题名称添加特殊标记。有关此主题的更多信息可以在ROS 2设计文档中找到。
使用之前定义的相同IDL文件,Fast DDS-Gen可以生成处理Fast DDS中新类型所需的代码。需要在Fast DDS应用程序中进行的更改,以便rosbag2能够与其通信,将通过使用Fast DDS-Gen从IDL自动生成的发布者/订阅者示例来进行说明。有关Fast DDS-Gen的详细指南可以在这里找到。
对于普通主题,ROS 2会向DDS主题名称添加命名空间“rt/”。ROS 2生成的类型的DataType名称是通过连接模块名称来构造的。对于此示例中使用的IDL,数据类型名称将为“fastdds_record_typesupport::idl::HelloWorld”。
创建一个与之前使用的ROS 2不同的新工作空间。将相同的IDL文件复制到其中,并运行Fast DDS-Gen生成TypeSupport和示例源文件:
这个命令将在当前文件夹中生成所需的头文件和源文件,用于构建TypeSupport,以及发布者和订阅者应用程序。
在修改Fast DDS-Gen示例时,应考虑ROS 2应用的主题和类型名称混淆,以便与rosbag2进行通信。在生成TypeSupport时使用了-typeros2
Fast DDS-Gen选项,生成的类型名称已经包含了ROS 2的命名规则混淆。然而,主题名称必须在发布者和订阅者应用程序中手动修改。在HelloWorldPublisher.cxx
和HelloWorldSubscriber.cxx
文件中查找create_topic
命令,并修改主题名称:
要构建此示例,请运行以下命令:
这将在build目录中创建一个名为HelloWorld的二进制文件,可用于启动发布者和订阅者应用程序。在终端中运行每个应用程序,并确认通信已建立。
15.12.1.4 eProsima DDS 录制和重放
为了使用生成的ROS 2 TypeSupport包,除了ROS 2安装外,还应该设置ROS 2工作空间。这样可以让rosbag2记录此示例中使用的数据类型。要开始记录发布者/订阅者应用程序之间交换的流量,必须将相应的ROS 2主题名称传递给rosbag2(不要与DDS主题名称混淆)。还要记得通过设置环境变量RMW_IMPLEMENTATION
来确保Fast DDS是ROS 2使用的中间件。
在发布者应用程序已经运行的情况下,将显示以下rosbag2日志发现信息:
rosbag2将继续创建一个名为rosbag2_<DATE>
的文件夹,其中包含一个SQLite数据库(扩展名为db3
),用于记录接收到的消息。文件夹中的一个YAML
文件提供了有关记录的元数据信息:类型和主题名称、记录的消息数量、记录持续时间等。可以使用此数据库文件的路径来重放记录的消息。在订阅者应用程序运行时,之前记录的流量将被重放。在停止rosbag2应用程序后,以重放模式重新运行它,运行以下命令。记录的消息将由rosbag2以其原始发布速率发布,订阅者应用程序将接收到这些消息:
15.13 请求-应答通信
本节将介绍如何在Fast DDS中配置两个应用程序之间的请求-应答通信。客户端应用程序将向服务器应用程序发送请求,服务器应用程序在处理请求后将向客户端应用程序发送回复。
15.13.1 总览
这种通信方式在DDS范式中涉及使用两个 主题:一个用于发送请求(请求主题),另一个用于发送回复(回复主题)。为了管理这些主题,涉及到四个DDS实体:每个主题一个DataReader和一个DataWriter。DDS通信架构将会是:
使 请求-应答 工作的关键是在客户端端将请求与回复相关联。Fast DDS API提供了SampleIdentity
来实现这一点。
完整的示例可以在 Fast DDS存储库 中找到。
15.13.2 开始
进行请求-应答
通信时,请执行以下步骤:
- 在一个IDL文件中定义两个结构。一个结构将用作请求主题的数据类型,另一个将用作回复主题的数据类型。
- 在客户端应用程序中,为请求创建一个DataWriter,并为回复创建一个DataReader。
- 在服务器应用程序中,为回复创建一个DataWriter,并为请求创建一个DataReader。
15.13.3 发送请求并存储分配的标识符
为了发送请求,客户端应用程序应该检索并存储分配给已发布样本的内部标识符。因此,应该使用重载的write()
函数来发布样本,其中第二个参数是指向WriteParams
对象的引用。分配的标识符将存储在WriteParams
的sample_identity()
属性中。
15.13.4 接收请求并发送与之关联的回复
当服务器应用程序接收到请求(例如通过on_data_available()
),它必须使用sample_identity
来检索请求的标识符。
处理完请求后,服务器应该将带有相关请求的回复发送给客户端。这是通过将存储的标识符分配给related_sample_identity()
来实现的。
15.13.5 为客户端标识回复
当客户端应用程序接收到回复(例如通过on_data_available()
),客户端应用程序应该确定接收到的回复是否是其请求所期望的回复。为此,客户端应用程序必须将存储的SampleIdentity
与传入的related_sample_identity
进行比较。
18 FastDDS 文档18 C++ API手册
Fast DDS作为Data Distribution Service (DDS)标准的实现,提供了DDS Data-Centric Publish-Subscribe (DCPS) Platform Independent Model (PIM) API,该API符合DDS规范。此外,它还提供了直接与底层的Real-time Publish-Subscribe (RTPS) API进行交互的功能,该API用于有线通信,符合RTPS标准。
本节介绍了Fast DDS提供的最常用的API。有关API参考的更多信息,请参阅Fast DDS API参考文档。