在上一章节中,我们观察了域参与者qos创建背后的一些行为,在本章中,我们将查看域参与者工厂是怎么创建域参与者的,与此同时,域参与者工厂又在这里面起了什么作用。(本篇文章来源于作者自己对于源码的理解,非常主观,如有错误敬请见谅)
创建域参与者的代码如下:
participant_ = DomainParticipantFactory::get_instance()->create_participant(0, pqos);
我们可以看见,域参与者是由域参与者工厂的一个单例模式进行创建的。
该单例模式的代码如下:
std::shared_ptr<DomainParticipantFactory> DomainParticipantFactory::get_shared_instance()
{
// Note we need a custom deleter, since the destructor is protected.
static std::shared_ptr<DomainParticipantFactory> instance(
new DomainParticipantFactory(),
[](DomainParticipantFactory* p)
{
delete p;
});
return instance;
}
我们可以看到这就是一个懒汉式的单例模式,但是我们可以看见他并没有创建锁来保证线程安全。我猜测这里是利用了C++11 及更新版本中静态局部变量初始化的线程安全特性来实现的,一个静态局部变量在其作用域第一次被访问时,C++ 保证该变量的初始化是线程安全的。这意味着即使多个线程同时试图访问该静态局部变量,编译器也会确保只有一个线程能够进行初始化,其他线程会等待初始化完成。此外工厂模式的使用场景一般也不涉及并发访问。
在了解完单例模式之后我们来观看函数创建域参与者内部的实现:
在输入参数之后,他会走头文件中的这个调用:
FASTDDS_EXPORTED_API DomainParticipant* create_participant(
DomainId_t domain_id,
const DomainParticipantQos& qos,
DomainParticipantListener* listener = nullptr,
const StatusMask& mask = StatusMask::all());
我们先来聊一聊这四个变量的是什么东西。首先DDS中使用 DomainId_t 来唯一标识一个域。在同一个域下你才可以互相通信,qos上章说了,listener则是状态监听器。DDS通过监听器提供底层状态事件异步回调的机制,每个实体状态在相应的实体监听器中均有相应的回调函数与之对应。DDS底层检测到该状态变化时,通过回调监听器的相应方法,用户即可获取该状态的值。statusMask则代表用于指定监听器感兴趣的状态变化,默认值为all,就是说对全部状态都感兴趣。但是因为不使用监听器,所以没啥用。
之后便来到了函数的主体:
DomainParticipant* DomainParticipantFactory::create_participant(
DomainId_t did,
const DomainParticipantQos& qos,
DomainParticipantListener* listener,
const StatusMask& mask)
{
load_profiles();
const DomainParticipantQos& pqos = (&qos == &PARTICIPANT_QOS_DEFAULT) ? default_participant_qos_ : qos;
DomainParticipant* dom_part = new DomainParticipant(mask);
#ifndef FASTDDS_STATISTICS
DomainParticipantImpl* dom_part_impl = new DomainParticipantImpl(dom_part, did, pqos, listener);
#else
eprosima::fastdds::statistics::dds::DomainParticipantImpl* dom_part_impl =
new eprosima::fastdds::statistics::dds::DomainParticipantImpl(dom_part, did, pqos, listener);
#endif // FASTDDS_STATISTICS
if (fastrtps::rtps::GUID_t::unknown() != dom_part_impl->guid())
{
{
std::lock_guard<std::mutex> guard(mtx_participants_);
using VectorIt = std::map<DomainId_t, std::vector<DomainParticipantImpl*>>::iterator;
VectorIt vector_it = participants_.find(did);
if (vector_it == participants_.end())
{
// Insert the vector
std::vector<DomainParticipantImpl*> new_vector;
auto pair_it = participants_.insert(std::make_pair(did, std::move(new_vector)));
vector_it = pair_it.first;
}
vector_it->second.push_back(dom_part_impl);
}
if (factory_qos_.entity_factory().autoenable_created_entities)
{
if (RETCODE_OK != dom_part->enable())
{
delete_participant(dom_part);
return nullptr;
}
}
}
else
{
delete dom_part_impl;
return nullptr;
}
return dom_part;
}
我们看到他先用load_profiles()导入了一些配置参数,然后查看qos是不是默认设置的qos,如果是则使用默认配置的qos否则使用之前配置的qos。并在之后根据域参与者创造了域参与者的实现类。需要说明的是DomainParticipant本身是一个抽象基类,提供了与 DDS 域参与者相关的公共接口。它定义了域参与者的操作,如创建和管理发布者,主题啊,订阅者啊之类的。而与之相关的DomainParticipantImpl是实现类,用于实现DomainParticipant的虚函数细节,它处理实际的 DDS 内部操作,如数据传输、连接管理、QoS 配置等。
现在我们看一看DomainParticipantImpl内部是如何创建的:
eprosima::fastdds::statistics::dds::DomainParticipantImpl* dom_part_impl =
new eprosima::fastdds::statistics::dds::DomainParticipantImpl(dom_part, did, pqos, listener);
我们可以看见,他传入了域参与者,域ID,QOS策略以及监听者。
DomainParticipantImpl的函数主体如下:
DomainParticipantImpl::DomainParticipantImpl(
DomainParticipant* dp,
DomainId_t did,
const DomainParticipantQos& qos,
DomainParticipantListener* listen)
: domain_id_(did)
, next_instance_id_(0)
, qos_(qos)
, rtps_participant_(nullptr)
, participant_(dp)
, listener_(listen)
, default_pub_qos_(PUBLISHER_QOS_DEFAULT)
, default_sub_qos_(SUBSCRIBER_QOS_DEFAULT)
, default_topic_qos_(TOPIC_QOS_DEFAULT)
, id_counter_(0)
#pragma warning (disable : 4355 )
, rtps_listener_(this)
{
participant_->impl_ = this;
PublisherAttributes pub_attr;
XMLProfileManager::getDefaultPublisherAttributes(pub_attr);
utils::set_qos_from_attributes(default_pub_qos_, pub_attr);
SubscriberAttributes sub_attr;
XMLProfileManager::getDefaultSubscriberAttributes(sub_attr);
utils::set_qos_from_attributes(default_sub_qos_, sub_attr);
TopicAttributes top_attr;
XMLProfileManager::getDefaultTopicAttributes(top_attr);
utils::set_qos_from_attributes(default_topic_qos_, top_attr);
// Pre calculate participant id and generated guid
participant_id_ = qos_.wire_protocol().participant_id;
if (!eprosima::fastrtps::rtps::RTPSDomainImpl::create_participant_guid(participant_id_, guid_))
{
EPROSIMA_LOG_ERROR(DOMAIN_PARTICIPANT, "Error generating GUID for participant");
}
/* Fill physical data properties if they are found and empty */
std::string* property_value = fastrtps::rtps::PropertyPolicyHelper::find_property(
qos_.properties(), parameter_policy_physical_data_host);
if (nullptr != property_value && property_value->empty())
{
property_value->assign(asio::ip::host_name() + ":" + std::to_string(utils::default_domain_id()));
}
property_value = fastrtps::rtps::PropertyPolicyHelper::find_property(
qos_.properties(), parameter_policy_physical_data_user);
if (nullptr != property_value && property_value->empty())
{
std::string username = "unknown";
if (RETCODE_OK == SystemInfo::get_username(username))
{
property_value->assign(username);
}
}
property_value = fastrtps::rtps::PropertyPolicyHelper::find_property(
qos_.properties(), parameter_policy_physical_data_process);
if (nullptr != property_value && property_value->empty())
{
property_value->assign(std::to_string(SystemInfo::instance().process_id()));
}
}
我们可以看到在第一部分,他先初始化了一部分参数。这些参数第一个不用说,肯定是域ID。第二个next_instance_id_用于跟踪下一个实例的 ID,可能在创建多个DomainParticipantImpl的时候使用,qos策略不用说,RTPS参与者用于存储与 RTPS(Real-Time Publish-Subscribe)协议相关的参与者对象接下来就是参与者监听器以及一些默认的qos了,id_count是生成唯一ID用于管理DomainparticpantImpl使用。
在往下看这里禁用特定的编译器警告(在这里是 4355),这个警告通常涉及使用this指针初始化成员变量的问题。这里使用this指针初始化rtps_listener_可能会报错。rtps_listener_是一个监听器,它需要知道当前的DomainparticpantImpl实例,因此使用this指针来初始化它。这样,监听器可以回调当前对象的方法。在之后把实例类与域参与者抽象类相绑定,为了之后域参者内部调用功能做准备。在之后的this其具体意义在于将当前的 DomainparticpantImpl实例与它所对应的 DomainParticipant
实例绑定起来.在设计模式中,这种构造方式常见于桥接模式或代理模式。通过将接口类(或抽象类 DomainParticipant
)中的成员变量 impl_
与当前具体实现类对象的指针 this
进行关联,可以在需要的时候通过 participant_
访问其具体实现。这也就是一种组合的实现方式。
接下来这一部分则是为了初始化默认的发布者、订阅者和主题属性::
PublisherAttributes pub_attr;
XMLProfileManager::getDefaultPublisherAttributes(pub_attr);
utils::set_qos_from_attributes(default_pub_qos_, pub_attr);
SubscriberAttributes sub_attr;
XMLProfileManager::getDefaultSubscriberAttributes(sub_attr);
utils::set_qos_from_attributes(default_sub_qos_, sub_attr);
TopicAttributes top_attr;
XMLProfileManager::getDefaultTopicAttributes(top_attr);
utils::set_qos_from_attributes(default_topic_qos_, top_attr);
我们先随便看一个他是怎么进行初始化的,其他两个其实都差不多。那我们看一看pub_attr是怎么被初始化的。我们先看一下PublisherAttributes的构造函数:
PublisherAttributes() = default;
virtual ~PublisherAttributes() = default;
我们可以看见,它采用的都是默认构造。在getDefaultPublisherAttributes中代码如下:
void XMLProfileManager::getDefaultPublisherAttributes(
PublisherAttributes& publisher_attributes)
{
publisher_attributes = default_publisher_attributes;
}
我们可以看到就是直接复制非常简单粗暴啊,而且PublisherAttributes类中并没有重写赋值运算符,因此会自动生成一个浅拷贝的复制运算符在这里运用。
我记得在上一章中我们在设置qos的时候提到了property的概念,property 类用于存储具体的属性键值对,这些属性可以包括各种配置参数,例如主机信息、用户信息、进程信息等。那Attributes和property的关系是什么呢?或者说Attributes类是什么呢?
-
PublisherAttributes 类
PublisherAttributes 是一个类,用于定义发布者的各种属性和 QoS 配置。这些属性包括但不限于:
- QoS 策略(如可靠性、持久性、历史记录等)
- 其他相关的设置
例如,PublisherAttributes可能有以下成员变量:
- qos:用于存储 QoS 设置。
- property_:一个集合,包含与发布者相关的额外属性(如自定义配置)。
-
properties_
成员properties_ 是一个集合(如map 或unordered_map),用于存储属性的键值对。在 DDS 中,properties_ 通常是一个存储属性的字典,可以用来传递额外的配置信息。
- 设置 QoS 时的属性:在设置 QoS 时,可能会将 QoS 参数作为属性放入 properties_ 中。属性是灵活的配置项,可以在运行时调整。
- 属性和 QoS 的关系:虽然 properties_ 和 qos_ 都是 PublisherAttributes 的一部分,但 properties_ 通常用于存储与 QoS 相关的额外或自定义的配置,而 qos_ 则包含标准的 QoS 配置。
总的来说PublisherAttributes用于封装发布者的所有属性和 QoS 设置。而properties则是前者中的一个集合,用于存储与 QoS 相关的额外配置或自定义属性。
之后便是根据Attributes对qos进行设置了,我们来看一看是怎么进行设置的
void set_qos_from_attributes(
PublisherQos& qos,
const PublisherAttributes& attr)
{
qos.group_data().setValue(attr.qos.m_groupData);
qos.partition() = attr.qos.m_partition;
qos.presentation() = attr.qos.m_presentation;
}
这其实就是三个qos策略,以上函数将返回以下变量
//!Presentation Qos, NOT implemented in the library.
PresentationQosPolicy presentation_;
//!Partition Qos, implemented in the library.
PartitionQosPolicy partition_;
//!Group Data Qos, implemented in the library.
GroupDataQosPolicy group_data_;
我们先看看这三个qos的功能,并且为什么选择在DomainparticpantImpl中配置这三个qos而不是其他qos.
1. PresentationQosPolicy
(展示QoS策略)
- 作用:定义数据在订阅者端如何呈现。它主要决定数据的访问顺序和分组方式,通过配置,用户可以确保数据按原始顺序递送或者以一致性组来进行处理。
- 属性:一般包含几个选项如访问范围(例如实例、主题和组)和一致性信号等。
- 必要性:尽管代码注释表明该策略“未在库中实现”,它的存在和配置还是可以在需要时控制数据的展示方式,这在一些应用场景中非常重要,尤其是需要保证数据的一致性和有序性的系统。
- 理由:即便未具体实现,也需保留其配置,便于未来扩展或在必要时启用。
2. PartitionQosPolicy
(分区QoS策略)
- 作用:提供一种逻辑上的数据分区机制。通过分区,发布者和订阅者可以属于不同的逻辑“数据空间”,从而实现隔离或逻辑分组。
- 属性:通常包含分区名称或列表,通过这些名称可以将相关的数据进行逻辑分类。
- 必要性:分区QoS策略是强大的数据分区和隔离工具。它允许开发者将不同的发布者和订阅者逻辑分组,以实现数据的分区处理和管理。
- 应用场景:对于大型系统或者复杂的分布式系统,分区提供了灵活性,使不同模块或子系统可以在同一个DDS域中操作而无需互相干扰。
- 实现:在你的代码中,该QoS策略确实已经在库中实现。
3. GroupDataQosPolicy
(组数据QoS策略)
- 作用:为参与者提供一种方法,以便附加应用程序级别的组数据。用于在参与者之间共享一些协商数据,以确保某些级别的一致性和协调。
- 属性:通常是一些二进制数据或自定义数据,可以包含参与者的额外信息。
- 必要性:组数据QoS策略用于在多个DomainParticipant之间共享应用程序级别的数据。这样可以在参与者之间实现某种程度的信息共享和协同。
选择设置这三个QoS策略是为了在不破坏系统稳定和结构的情况下,解决一些基础且广泛存在的需求。
这里的分区似乎和域也有点像。那么我们在这里指出他们的关系:
DDS中的“域”(Domain)
1. 概念
- 定义:域(Domain)是DDS系统的逻辑分隔单元,用于将整个DDS网络划分为独立的通信空间。每个域都有一个唯一的域ID。
- 功能:域之间彼此隔离,属于不同域的参与者互相不能看到对方的数据。这样可以有效地将不同应用或环境中的数据隔离开来。
2. 使用场景
- 防止干扰:不同的项目或系统可以部署在不同的域中,确保数据和消息不会因为域的重叠而干扰。
- 资源管理:通过划分域,可以更好地管理和优化资源。
QoS策略中的“分区”(Partition)
1. 概念
- 定义:分区(Partition)是DDS QoS策略中的一部分,用于将单个域内的数据流进一步划分为多个子集。它提供了一种在同一个域内进行更细粒度数据隔离和管理的方法。
- 功能:通过分区,不同的发布者和订阅者可以在同一个域中通过指定相同的分区名称来匹配彼此的消息。换句话说,分区让你可以在一个域里创建隔离的通信子集。
2. 使用场景
- 逻辑分组:一个复杂系统可以利用分区将不同模块或功能的数据流区分开来。例如,不同部门的数据流可以划分为不同的分区。
- 管理隔离:在同一域中,根据需求将数据流的访问权限、流量等进行控制和管理。
接下来我们来看看这一句代码
// Pre calculate participant id and generated guid
participant_id_ = qos_.wire_protocol().participant_id;
if (!eprosima::fastrtps::rtps::RTPSDomainImpl::create_participant_guid(participant_id_, guid_))
{
EPROSIMA_LOG_ERROR(DOMAIN_PARTICIPANT, "Error generating GUID for participant");
}
这行注释告诉我们,代码的目的是预先计算参与者的ID和生成该参与者的全局唯一标识符(GUID)。
我们先来看看GUID是什么
//!Pre-calculated guid
fastrtps::rtps::GUID_t guid_;
他的结构体源码如下
struct FASTDDS_EXPORTED_API GUID_t
{
//!Guid prefix
GuidPrefix_t guidPrefix;
//!Entity id
EntityId_t entityId;
}
我们可以看到GUID由前缀和实体ID两部分组成。他的作用是标识唯一实体,在分布式系统中,实体的唯一标识对数据的通信与交换至关重要。毕竟如果你要订阅的节点都错了,那么接收的数据怎么都不对了对吧。有GUID_t的定义我们可以看到他由两部分组成,分别是占12字节的GuidPrefix_t和占4字节的EntityId_t组成。前缀通常用于标识特定的参与者(Participant),而实体ID用于标识特定的参与者下的实体,例如发布者(Publisher)、订阅者(Subscriber)、主题(Topic)等。
对于前者称为前缀他是由0x010f+host_id+process_id+random_number+participant_id组成,除了最后的participant_id其余的均占2字节。其中host_id是根据当前ipv4地址做md5验算所得结果。前8个字节固定不变,participant_id则根据个数从1开始自增。
而后一半EntityId_t则是实体ID。根据实体的不同而变化。
#define ENTITYID_UNKNOWN 0x00000000
#define ENTITYID_RTPSParticipant 0x000001c1
#define ENTITYID_SEDP_BUILTIN_TOPIC_WRITER 0x000002c2
#define ENTITYID_SEDP_BUILTIN_TOPIC_READER 0x000002c7
#define ENTITYID_SEDP_BUILTIN_PUBLICATIONS_WRITER 0x000003c2
#define ENTITYID_SEDP_BUILTIN_PUBLICATIONS_READER 0x000003c7
#define ENTITYID_SEDP_BUILTIN_SUBSCRIPTIONS_WRITER 0x000004c2
#define ENTITYID_SEDP_BUILTIN_SUBSCRIPTIONS_READER 0x000004c7
#define ENTITYID_SPDP_BUILTIN_RTPSParticipant_WRITER 0x000100c2
#define ENTITYID_SPDP_BUILTIN_RTPSParticipant_READER 0x000100c7
#define ENTITYID_P2P_BUILTIN_RTPSParticipant_MESSAGE_WRITER 0x000200C2
#define ENTITYID_P2P_BUILTIN_RTPSParticipant_MESSAGE_READER 0x000200C7
#define ENTITYID_P2P_BUILTIN_PARTICIPANT_STATELESS_WRITER 0x000201C3
#define ENTITYID_P2P_BUILTIN_PARTICIPANT_STATELESS_READER 0x000201C4
首先他先取到了前缀中的participant_id然后掉了这个函数
bool RTPSDomainImpl::create_participant_guid(
int32_t& participant_id,
GUID_t& guid)
{
bool ret_value = get_instance()->reserve_participant_id(participant_id);
if (ret_value)
{
guid_prefix_create(participant_id, guid.guidPrefix);
guid.entityId = c_EntityId_RTPSParticipant;
}
return ret_value;
}
我们可以看见他设置了传入的GUID的前缀和实体ID,而reserve_participant_id的作用则是分配一个不重复的participant_id
bool RTPSDomainImpl::reserve_participant_id(
int32_t& participant_id)
{
std::lock_guard<std::mutex> guard(m_mutex);
if (participant_id < 0)
{
participant_id = getNewId();
}
else
{
if (m_RTPSParticipantIDs[participant_id].reserved == true)
{
return false;
}
m_RTPSParticipantIDs[participant_id].reserved = true;
}
return true;
}
前缀的创建就不看了要不就写太多了。
至于往后的部分就比较简单粗暴了。
/* Fill physical data properties if they are found and empty */
std::string* property_value = fastrtps::rtps::PropertyPolicyHelper::find_property(
qos_.properties(), parameter_policy_physical_data_host);
if (nullptr != property_value && property_value->empty())
{
property_value->assign(asio::ip::host_name() + ":" + std::to_string(utils::default_domain_id()));
}
property_value = fastrtps::rtps::PropertyPolicyHelper::find_property(
qos_.properties(), parameter_policy_physical_data_user);
if (nullptr != property_value && property_value->empty())
{
std::string username = "unknown";
if (RETCODE_OK == SystemInfo::get_username(username))
{
property_value->assign(username);
}
}
property_value = fastrtps::rtps::PropertyPolicyHelper::find_property(
qos_.properties(), parameter_policy_physical_data_process);
if (nullptr != property_value && property_value->empty())
{
property_value->assign(std::to_string(SystemInfo::instance().process_id()));
}
还记得我们qos最开始设置的空值吗?这里就是把那里的value补上。那么至Domainparticipantimpl的构造函数就结束了。他主要就干了几件事情。1.因为他是Domainparticipant的具体内部细节实现类,所以它采用了组合的方式,传入Domainparticipant并且把自己与对应的Domainparticipant绑定,以便之后能够调用具体的方法。2.他设置了3个QOS 3.设置了全局唯一标识符GUID以标识对应的实体.4.对之前的qos里面的property为空的进行了设置。
之后我们回到之前的代码:往下
if (fastrtps::rtps::GUID_t::unknown() != dom_part_impl->guid())
{
{
std::lock_guard<std::mutex> guard(mtx_participants_);
using VectorIt = std::map<DomainId_t, std::vector<DomainParticipantImpl*>>::iterator;
VectorIt vector_it = participants_.find(did);
if (vector_it == participants_.end())
{
// Insert the vector
std::vector<DomainParticipantImpl*> new_vector;
auto pair_it = participants_.insert(std::make_pair(did, std::move(new_vector)));
vector_it = pair_it.first;
}
vector_it->second.push_back(dom_part_impl);
}
if (factory_qos_.entity_factory().autoenable_created_entities)
{
if (RETCODE_OK != dom_part->enable())
{
delete_participant(dom_part);
return nullptr;
}
}
}
else
{
delete dom_part_impl;
return nullptr;
}
return dom_part;
}
他先检查 dom_part_impl
的 guid
是否已设置(即不是未知GUID)。如果 guid
是未知的,删除该参与者实现并返回 nullptr
。这通常用来确保每个参与者都有唯一的标识。如果确定guid已经正常设置,则使用区域锁 std::lock_guard
确保在操作 participants_
时的线程安全。定义一个迭代器 VectorIt
,用于遍历 participants_
,这是一个 std::map
,键是 DomainId_t
,值是 std::vector<DomainParticipantImpl*>
。查找给定的 did
(域ID)是否已经存在于 participants_
中。如果不存在,创建一个新的向量并插入到 participants_
中,然后更新迭代器。如果存在则直接将新建的DomainParticipantImpl
插入到对应的的域ID的pair中。也就是域内的节点才能相互感知。再往后如果 factory_qos_
的 entity_factory
配置了 autoenable_created_entities
(自动启用创建的实体),尝试启用 dom_part
。如果启用失败,调用 delete_participant
删除该参与者并返回 nullptr
。自动启用实体的一个主要作用是简化用户的操作和管理,让用户在创建实体时不需要手动去启用它们。原始的手动启用过程往往涉及额外的代码和上下文管理,通过自动启用,可以减少代码冗余,提高代码的可读性和维护性。以DDS(Data Distribution Service)为例,DDS实体如 DomainParticipant
、Publisher
、Subscriber
和 DataWriter
通常需要先创建,然后再启用。自动启用实体则是保证这些实体在创建时立即启用并准备好使用。至此域参与者就可以返回了。