目录
Domain
DDS中的Domain是由一个整数定义,通过在创建DomainParticipant时指定。这是一种逻辑概念而非物理概念。
Domain域代表一个独立的通信网络。它在共享相同通信基础设施的实体之间创建了逻辑上的分离。在概念上,它可以被视为链接运行在同一域上的所有应用程序,并将它们与运行在不同域的应用程序隔离的虚拟网络。这样,一些独立的分布式应用程序可以在同一物理网络中共存,而不会相互干扰,甚至不需要互相感知。
每个domain有一个唯一的uint32的domainId,由应用程序在创建DomainParticipant时指定。当一个应用程序需要加入domain时就需要使用这个domainId创建一个DomainParticipant。
每个DomainParticipant都会把它所在的DomainId作为其属性。当两个DomainParticipant尝试进行通信时,他们的通讯层会首先检查这两个DomainParticipant是否处于同一Domain(即比较他们的DomainId是否相同)。只有当他们位于相同的Domain时,他们才能够进行通信。如果他们的DomainId不同,那么他们就无法直接通信,这就实现了不同Domain之间的隔离。
这种隔离机制可以确保同一物理网络内的不同DDS应用能够独立运行,彼此不受干扰。无论是数据的传输,还是资源的分配,都是在各自的Domain内进行的,不会影响到其他Domain。
DomainParticipant
每个DomainParticipant只归属于一个Domain,包含所有和domain相关的实体。同时它也是Publisher,Subscriber和Topic的生产工厂。
DomainParticipant的创建使用DomainParticipantFactory这个单例。
使用DomainParticipant的GuidPrefix_t来区分是否在同一主机运行。同一主机中两个participants的GuidPrefix_t的前四个字节是相同的。可以通过API:is_on_same_host_as() 来检查这一条件。
用途
- 作为所有Entity的容器的对象
- Publisher, Subscriber, Topic, and MultiTopic Entity对象的工厂
- Participant(即DomainParticipant)代表应用程序在一个通信平面上的参与,这个通信平面能够将运行在相同物理计算机集上的应用程序彼此隔离。一个域(Domain)建立了一个“虚拟网络”,链接所有共享相同DomainId的应用程序,并将其与运行在不同域的应用程序隔离开来。通过这种方式,几个独立的分布式应用程序可以在同一个物理网络中并存,而不会相互干扰,甚至无需知道彼此的存在。所以,Participant在DDS中的作用不仅局限于数据的发布和订阅,还包括在分布式环境中提供应用程序隔离。
- DomainParticipant 提供了对Domain的管理服务。它提供了一些操作,允许应用程序在本地“忽略”有关特定参与者(ignore_participant)、发布(ignore_publication)、订阅(ignore_subscription)或主题(ignore_topic)的任何信息。
“忽略”操作的功能是,可以让应用程序选择性地忽略某些特定的数据,即使这些数据在其所在的域内。这样,应用程序就可以筛选出它真正关心的数据,不必处理所有的数据,从而提高了处理的效率。
对第四点的补充:
这些操作都是通过DomainParticipant的接口提供的,应用程序可以通过调用这些接口来执行相应的操作。例如,如果一个应用程序不关心某个特定的主题,那么它就可以调用ignore_topic接口,以后就不会再接收到有关这个主题的任何数据。
这种机制不仅可以提高数据处理的效率,还可以提高系统的灵活性,因为它允许应用程序根据其实际需求来选择处理哪些数据。
GUID_t的创建
GUID是RTPS协议的一部分,GUID_t
结构体的定义也位于RTPS层,由两部分组成:前缀guidPrefix
和 entityId
。所有由同一个participant创建的实体(如发布者、订阅者、数据写者、数据读者等)都共享相同的guidPrefix
。entityId
则是用来在同一个参与者下唯一地标识一个实体。GUID_t定义如下:
struct RTPS_DllAPI GUID_t
{
//!Guid prefix
GuidPrefix_t guidPrefix;
//!Entity id
EntityId_t entityId;
// ...
};
其中,guidPrefix
占12个字节,entityId
占4个字节;guidPrefix在抓包中显示分为三个部分:hostId, appId, instancdId, 下面代码说明
《抓包.png》
创建流程:
DomainParticiapnt
构造时创建GUID_t
。
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");
}
分析这段代码:
- 首先创建
participant_id_
。participant_id_
是从qos配置中读取,qos中默认为-1;进入RTPSDomainImpl::create_participant_guid
函数后,会判断participant_id_
如果为-1,就自动创建新的id,默认从0开始,依次累加,如果qos中指定但和其他id冲突,就会直接报错导致无法生成guid:
uint32_t RTPSDomainImpl::getNewId()
{
uint32_t i = 0;
while (m_RTPSParticipantIDs[i].reserved || m_RTPSParticipantIDs[i].used)
{
++i;
}
m_RTPSParticipantIDs[i].reserved = true;
return i;
}
participant_id
创建完成之后开始创建GuidPrefix_t
,GuidPrefix_t
的构造分两部分进行:
第一部分,前8个字节,在GuidUtils
这个单例构造时创建
GuidUtils() {
prefix_.value[0] = c_VendorId_eProsima[0];
prefix_.value[1] = c_VendorId_eProsima[1];
uint16_t host_id = SystemInfo::instance().host_id();
prefix_.value[2] = static_cast<octet>(host_id & 0xFF);
prefix_.value[3] = static_cast<octet>((host_id >> 8) & 0xFF);
int pid = SystemInfo::instance().process_id();
prefix_.value[4] = static_cast<octet>(pid & 0xFF);
prefix_.value[5] = static_cast<octet>((pid >> 8) & 0xFF);
std::random_device generator;
std::uniform_int_distribution<uint16_t> distribution(0, std::numeric_limits<uint16_t>::max());
uint16_t rand_value = distribution(generator);
prefix_.value[6] = static_cast<octet>(rand_value & 0xFF);
prefix_.value[7] = static_cast<octet>((rand_value >> 8) & 0xFF);
}
判断是否为同一个进程可以通过guid的前8个字节(在服务发现中均通过这种方式判断):
bool GuidPrefix_t::is_on_same_process_as(
const GuidPrefix_t& other_guid_prefix) const
{
return memcmp(value, other_guid_prefix.value, 8) == 0;
}
GuidPrefix_t的后四个字节由participant_id组成
void guid_prefix_create(
uint32_t participant_id,
GuidPrefix_t& guid_prefix) const
{
// Use precalculated vendor-host-process part of the prefix
std::copy(prefix_.value, prefix_.value + 8, guid_prefix.value);
// Add little endian serialization of participant_id
guid_prefix.value[8] = static_cast<octet>(participant_id & 0xFF);
guid_prefix.value[9] = static_cast<octet>((participant_id >> 8) & 0xFF);
guid_prefix.value[10] = static_cast<octet>((participant_id >> 16) & 0xFF);
guid_prefix.value[11] = static_cast<octet>((participant_id >> 24) & 0xFF);
}
GuidPrefix_t
创建完成之后开始创建entityID
,对participant来说entityID
是固定值:c_EntityId_RTPSParticipant
,Fast DDS中定义为:#define ENTITYID_RTPSParticipant 0x000001c1
GuidPrefix_t
中提供方法用于判断是否是同一进程,这在服务发现时用到:
bool GuidPrefix_t::is_on_same_process_as(
const GuidPrefix_t& other_guid_prefix) const
{
return memcmp(value, other_guid_prefix.value, 8) == 0;
}
即通过GuidPrefix_t的前8个字节判断是否是同一进程内的通信。
GUID_t 用途总结
- 第0和第1个字节是
vendorID
,表示实现DDS协议的供应商的ID,不同的供应商有不同的值;第2和第3个字节是host_id
,这个值是通过ip地址得到的,同一台主机这两个字段值相同,所以可以通过这两个字段判断是否在同一主机,这4个字节组成抓包中的hostId(可以判断是否是同一主机) - 第4和第5个字节是进程id,同一进程有相同的id;第6和第7个字节是随机生成的,这四个字节组成抓包中的appId
- 第8到第11字节由
participant_id
经过位运算组成instanceId(可以判断是否是同一participant) - 最后四字节为entityid,唯一标识同一participant中的不同entity
每个位于同一participant
中的entity
(Publisher, Subscriber, DataWriter, DataReader, Topic
)都共享participant
的GuidPrefix_t
。
创建
DomainParticipantFactory* factory = DomainParticipantFactory::get_instance();
participant_ = factory->create_participant(0, pqos); // 第一个参数domainid和第二个参数qos必选,listener和statusmask可选
if(nullptr == participant_with_default_attributes) {
// Error
return;
}
基于Profile创建, domainId和profile name是必须的,listener和StatusMask是可选的:
// First load the XML with the profiles
DomainParticipantFactory::get_instance()->load_XML_profiles_file("profiles.xml");
// Create a DomainParticipant using a profile and no Listener
DomainParticipant* participant_with_profile =
DomainParticipantFactory::get_instance()->create_participant_with_profile(0, "participant_profile");
if (nullptr == participant_with_profile) {
return;
}
删除
只有当所有属于这个participant的entities(Publisher,Subscriber,Topic)都被删除时可以删除DomainParticipant,否则报错。可以通过 delete_contained_entities()
函数删除所有entities。
if (participant->delete_contained_entities() != ReturnCode_t::RETCODE_OK) {
// DomainParticipant failed to delete the entities it created.
return;
}
// Delete the DomainParticipant
if (DomainParticipantFactory::get_instance()->delete_participant(participant) != ReturnCode_t::RETCODE_OK) {
// Error
return;
}
DomainParticipantFactory
单例类,通过get_instance()获取对象实例,唯一的目的是用于创建和销毁DomainParticipant对象实例;通过DomainParticipantFactoryQos配置Qos,Qos只能通过Domain Participant Factory::set_qos()来设置
DomainParticipantQos
DomainParticipant的行为可以通过DomainParticipantQos的Qos值来修改,Qos值可以在创建时设置,也可以后续使用DomainParticipant::set_qos()
函数来设置。默认值为PARTICIPANT_QOS_DEFAULT
。也可以使用 set_default_participant_qos() 函数设置qos。
DomainParticipant的Qos有以下内容:
UserDataQosPolicy user_data()
EntityFactoryQosPolicy entity_factory()
ParticipantResourceLimitsQos allocation()
PropertyPolicyQos properties()
WireProtocolConfigQos wire_protocol()
TransportConfigQos transport() and setup_transports()
FlowControllersQos flow_controllers()
ThreadSettings builtin_controllers_sender_thread()
ThreadSettings timed_events_thread()
ThreadSettings discovery_server_thread()
ThreadSettings security_log_thread()
DomainParticipantListener
DomainPariticipant有一个DomainParticipantListener的抽象类,用户可以继承这个抽象类实现响应函数用于通知DomainParticipant实例状态的改变。DomainParticipantListener继承自TopicListener,PublisherListener和SubscriberListener,因此它可以监听任何实体的事件通知。
当有事件发生时,总是先通知处理该事件的具体的监听器,因此DomainParticipantListener从其他监听器继承的回调函数只有在没有其他实体能够处理该事件的情况下会被调用(该实体没有附加监听器u后者StatusMask禁用了回调)
有以下回调函数:
on_participant_discovery()
:服务发现发现participanton_subscriber_discovery()
:服务发现过程中发现新的Subscriberon_publisher_discovery()
:服务发现过程中发现新的Publisheron_type_discovery()
:on_type_dependencies_reply()
:on_type_information_received()
:
Partitions
todo
RTPSParticipant
RTPSParticipant的构造大概分为以下几部分:
内置transport的创建
单播和组播初始化
参数默认值:
// 默认值
uint16_t portBase; // 7400.
uint16_t domainIDGain; // 250.
uint16_t participantIDGain; // 2.
uint16_t offsetd0; // 0.
uint16_t offsetd1; // 10.
uint16_t offsetd2; // 1.
uint16_t offsetd3; // 11.
组播端口值:uint32_t port = portBase + domainIDGain * domainId + offsetd0;
单播端口值:uint32_t port = portBase + domainIDGain * domainId + offsetd1 + participantIDGain * RTPSParticipantID;
组播IP地址:DEFAULT_METATRAFFIC_MULTICAST_ADDRESS = "239.255.0.1"
,通过函数getDefaultMetatrafficMulticastLocators
获取;
单播IP地址,被设置为0:
bool UDPv4Transport::getDefaultMetatrafficUnicastLocators(
LocatorList& locators,
uint32_t metatraffic_unicast_port) const
{
Locator locator;
locator.kind = LOCATOR_KIND_UDPv4;
locator.port = static_cast<uint16_t>(metatraffic_unicast_port);
locator.set_Invalid_Address(); // 这里被初始化为0
locators.push_back(locator);
return true;
}
接着开始调用NormalizeLocator
:
LocatorList UDPv4Transport::NormalizeLocator(
const Locator& locator)
{
LocatorList list;
if (IPLocator::isAny(locator)) // 单播都是0,所以可以继续
{
std::vector<IPFinder::info_IP> locNames;
get_ipv4s(locNames, false, false);
for (const auto& infoIP : locNames)
{
auto ip = asio::ip::address_v4::from_string(infoIP.name);
if (is_interface_allowed(ip))
{
Locator newloc(locator);
// 这里获取本地ip地址并最终存放到m_att.builtin.metatrafficUnicastLocatorList变量中
IPLocator::setIPv4(newloc, infoIP.locator);
list.push_back(newloc);
}
}
if (list.empty())
{
Locator newloc(locator);
IPLocator::setIPv4(newloc, "127.0.0.1");
list.push_back(newloc);
}
}
else
{
list.push_back(locator);
}
return list;
}
通过对NormalizeLocator
函数的调用,最终确定组播地址为239.255.0.1,单播地址为获取的本地地址,比如我这里docker环境中的地址是172.17.0.3;同时,将m_att.builtin.initialPeersList
值初始化为组播地址值:
// Initial peers
if (m_att.builtin.initialPeersList.empty())
{
m_att.builtin.initialPeersList = m_att.builtin.metatrafficMulticastLocatorList;
}
通过对函数get_default_unicast_locators()
的调用,将默认的单播地址也初始化为本地地址;
然后开始通过createReceiverResources
函数创建接收资源:
- 创建
ReceiverResource
对象(每个locator对应一个ReceiverResource
) - 每个
ReceiverResource
对象对应创建一个MessageReceiver
对象,并作为ReceiverResource
对象的一个属性保存 - 将创建好的
ReceiverResource
实例添加到RTPSParticipantImpl
的m_receiverResourcelist
字段中。
创建send buffer
创建SendBuffersManager对象
// Create buffer pool
send_buffers_.reset(new SendBuffersManager(num_send_buffers, allow_growing_buffers));
send_buffers_->init(this);
FlowController对象的创建
FlowControllerFactory对象在初始化时,构造了三个或四个FlowControllerImpl相关对象:
FlowControllerImpl<FlowControllerPureSyncPublishMode, FlowControllerFifoSchedule>
,一直使用同步方式发送数据;FlowControllerImpl<FlowControllerSyncPublishMode, FlowControllerFifoSchedule>
:尝试以同步方式发送数据,即发送一条消息后会等待确认收到该消息的反馈再发送下一条。如果收到反馈需要花费大量时间,它会自动切换到异步模式,以避免阻塞数据的发送FlowControllerImpl<FlowControllerAsyncPublishMode, FlowControllerFifoSchedule>
:异步模式发送数据FASTDDS_STATISTICS
模式下异步发送数据
这些对象将会在创建DataWriter时使用
内置协议的创建
第一部分,服务发现 请移步至 服务发现
第二部分,心跳 请移步至 Fast DDS中的保活机制