车载消息中间件FastDDS 源码解析(一)FastDDS 介绍和使用
车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上)
车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)
车载消息中间件FastDDS 源码解析(四)RtpsParticipant的创建(下)
车载消息中间件FastDDS 源码解析(五)BuiltinProtocols(上)
车载消息中间件FastDDS 源码解析(六)BuiltinProtocols(中)EDP
车载消息中间件FastDDS 源码解析(七)BuiltinProtocols(下)WLP&TypeLookupManager
车载消息中间件FastDDS 源码解析(八)TimedEvent
在前面几篇中我们介绍了RTPSParticipantImpl的组成部分,现在我们看一下这个RTPSParticipantImpl是如何运转的。
1.PDP发现机制的启动
在第二篇 车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上) 1.2节时序图的第5步中,调用到了RTPSParticipantImpl::enable。
这个就是启动发现机制。就是在fastdds中所有的组件都构建完毕后,启动了fastdds。
1.1时序图
sequenceDiagram
participant RTPSParticipantImpl
participant BuiltinProtocols
participant PDP
RTPSParticipantImpl ->> RTPSParticipantImpl: 1.enable
RTPSParticipantImpl ->> BuiltinProtocols: 2.enable
BuiltinProtocols ->> PDP: 3.enable
BuiltinProtocols ->> PDPSimple: 4.announceParticipantState
PDPSimple ->> PDP: 5.announceParticipantState
BuiltinProtocols ->> PDP:6.resetParticipantAnnouncement
PDP ->> RTPSParticipantImpl:7.enableReader
- RTPSParticipantImpl::enable 主要干了两件事
a.调用了BuiltinProtocols的enable函数
b.Messagereceiver 与Receiverresource关联,Receiverresource会不断监听socket,如果有收到消息,就交给Messagereceiver来处理
- BuiltinProtocols的enable函数 主要是调用了
a.PDP::enable函数 见3
b.PDPSimple的announceParticipantState函数 见4
c.resetParticipantAnnouncement 这个函数是 见5
- PDP::enable函数 主要是初始化了两个TimedEvent: lease_duration_event 和 resend_participant_info_event_
lease_duration_event 会周期性的check 这个remote_participant 是否还存在,不存在的话,需要把remote_participant移除
resend_participant_info_event_ 看名字就是知道这个event 的功能,就是发送pdp消息
- PDPSimple::announceParticipantState函数调用
a.PDP::announceParticipantState函数 如果是true,表示会新建一个change 放入到history中
b.StatelessWriter的unsent_changes_reset 如果没有新change,就发送history中之前的change
这个函数调用flow_controller*->add_new_sample 通过flow_controller* 发送出去
- PDP::announceParticipantState函数 主要干了2件事情
a.创建并初始化一个change
将Participant的信息写入change
b.将change放入到WriterHistory中去
-
PDP:resetParticipantAnnouncement 将resend_participant_info_event_ 启动
-
最终调用RTPSParticipantImpl:enbaleReader,将messagereceiver与endpoint(reader 或者writer) 关联
1.2源码解析
在RTPSParticipantImpl初始化完毕后,我们调用RTPSParticipantImpl::enable,这个enable 函数主要负责participant启动,往外发pdp消息,同时接收pdp消息。
步骤1:
void RTPSParticipantImpl::enable()
{
mp_builtinProtocols->enable();
//Start reception
for (auto& receiver : m_receiverResourcelist)
{
receiver.Receiver->RegisterReceiver(receiver.mp_receiver);
}
}
这个函数主要干了2件事:
1.调用BuiltinProtocols::enable
2.messagereceiver 与receiverresource 关联在一起
步骤2:
void BuiltinProtocols::enable()
{
if (nullptr != mp_PDP)
{
mp_PDP->enable();
mp_PDP->announceParticipantState(true);
mp_PDP->resetParticipantAnnouncement();
}
}
这里面调用了3个函数,下面分别介绍
1.PDP的函数 enbale
2.PDP的函数announceParticipantState
3.PDP的函数resetParticipantAnnouncement
步骤3:
bool PDP::enable()
{
// It is safe to call enable() on already enable PDPs
if (enabled_)
{
return true;
}
// Create lease events on already created proxy data objects
for (ParticipantProxyData* pool_item : participant_proxies_pool_)
{
pool_item->lease_duration_event = new TimedEvent(mp_RTPSParticipant->getEventResource(),
[this, pool_item]() -> bool
{
check_remote_participant_liveliness(pool_item);
return false;
}, 0.0);
}
resend_participant_info_event_ = new TimedEvent(mp_RTPSParticipant->getEventResource(),
[&]() -> bool
{
announceParticipantState(false);
set_next_announcement_interval();
return true;
},
0);
set_initial_announcement_interval();
enabled_.store(true);
// Notify "self-discovery"
getRTPSParticipant()->on_entity_discovery(mp_RTPSParticipant->getGuid(),
get_participant_proxy_data(mp_RTPSParticipant->getGuid().guidPrefix)->m_properties);
return builtin_endpoints_->enable_pdp_readers(mp_RTPSParticipant);
}
这个函数主要干了3件事:
这里面有个两个TimedEvent:
1.创建了一个TimedEvent :lease_duration_event
lease_duration_event 会周期性的check 这个remote_participant 是否还存在,不存在的话,不存在就把remote_participant移除
2.resend_participant_info_event_
resend_participant_info_event_ 看名字就是知道这个event 的功能,就是重新发送pdp消息,里面主要调用了2个函数
announceParticipantState(false); 如果是false 并不会将新change加入到history中,会发送在history 中的change。
set_next_announcement_interval();设置下一次触发resend_participant_info_event_的时间间隔
3.PDP::set_initial_announcement_interval
设置resend_participant_info_event_的时间间隔
4.builtin_endpoints_->enable_pdp_readers
这个函数主要就是启动reader,能够接收pdp消息
最终调用的是RTPSParticipantImpl::enableReader
也就是步骤7
void PDP::set_initial_announcement_interval()
{
if ((initial_announcements_.count > 0) && (initial_announcements_.period <= c_TimeZero))
{
// Force a small interval (1ms) between initial announcements
EPROSIMA_LOG_WARNING(RTPS_PDP, "Initial announcement period is not strictly positive. Changing to 1ms.");
initial_announcements_.period = { 0, 1000000 };
}
set_next_announcement_interval();
}
设置initial_announcements_.period 为100ms
set_next_announcement_interval 这个是设置发送下一次的消息的时间
void PDP::set_next_announcement_interval()
{
if (initial_announcements_.count > 0)
{
--initial_announcements_.count;
resend_participant_info_event_->update_interval(initial_announcements_.period);
}
else
{
resend_participant_info_event_->update_interval(m_discovery.discovery_config.leaseDuration_announcementperiod);
}
}
initial_announcements_.count 默认值是5
initial_announcements_.period 默认值是100ms
在初始化的时候,发送pdp消息,默认间隔是100ms 发送5次
5次发送完毕之后,发送间隔是leaseDuration_announcementperiod,默认设置为3s
这些都是可配置的,我们在使用dds的时候可以配置这些参数。
总结一下就是发送pdp消息,在初始化阶段,默认发送5次,每次间隔100ms,
(其实是6次,在初始化的时候,先发送一条,然后每隔100ms 发送一次),发送完毕之后,
每隔3s发送一次pdp消息。
我们看一下tcpdump抓的包
在一 处标识的是发送的时间,我们可以清楚的看到,第一次发送时间是0.106834s,第二次是0.205335s,发送消息的时间间隔约为0.1s大概是 100ms,因为发送的时间在网络层会有一些误差,所以并不是完美的100ms。
一共发了6条pdp消息,第一条是初始化阶段开始的时候发送的pdp消息,之后5条是通过timeevent发送的初始化消息,每隔100ms发送一次,发送了5次。
二处显示的消息类型是RTPS消息。
三处表明这个消息的大概内容。在这里DATA(p)表示这是一个pdp 的发现消息。
步骤4:
void PDPSimple::announceParticipantState(
bool new_change,
bool dispose,
WriteParams& wp)
{
if (enabled_)
{
auto endpoints = static_cast<fastdds::rtps::SimplePDPEndpoints*>(builtin_endpoints_.get());
StatelessWriter& writer = *(endpoints->writer.writer_);
WriterHistory& history = *(endpoints->writer.history_);
PDP::announceParticipantState(writer, history, new_change, dispose, wp);
if (!(dispose || new_change))
{
//将history中的消息都发送出去
writer.unsent_changes_reset();
}
}
}
这个函数主要干了2件事情:
1.PDP::announceParticipantState
这就是步骤5,这个我们在下面详细讨论
2.StatelessWriter的unsent_changes_reset函数
也就是步骤6:
bool new_change, 表示是否需要创建一个new change,false表示不创建,使用history中的消息。true表示重新创建。
步骤6
void PDP::resetParticipantAnnouncement()
{
if (resend_participant_info_event_)
{
resend_participant_info_event_->restart_timer();
}
}
resend_participant_info_event_ 重新开始
步骤7:
//builtin_endpoints_->enable_pdp_readers 最终调用的是RTPSParticipantImpl::enableReader
bool RTPSParticipantImpl::enableReader(
RTPSReader* reader)
{
if (!assignEndpointListenResources(reader))
{
return false;
}
return true;
}
assignEndpointListenResources 这个函数在builtinprotocols这个章节介绍过。
就是将messagereceiver与endpoint(reader 或者writer) 关联
2.PDPSimple::announceParticipantState
这个函数主要就是发送pdp消息
2.1时序图
sequenceDiagram
participant PDP
participant PDPSimple
participant RTPSWriter
participant WriterHistory
participant StatelessWriter
participant FlowControllerImpl
PDPSimple ->> PDP: 1.announceParticipantState
PDP ->> RTPSWriter: 2.new_change
RTPSWriter ->> WriterHistory: 3.add_change
WriterHistory ->> WriterHistory: 4.add_change_
WriterHistory ->> WriterHistory: 5.prepare_and_add_change
WriterHistory ->> WriterHistory: 6.notify_writer
WriterHistory ->> StatelessWriter: 7.unsent_change_added_to_history
StatelessWriter ->> FlowControllerImpl: 8.add_new_sample
PDPSimple ->> StatelessWriter:9.unsent_changes_reset
StatelessWriter ->> FlowControllerImpl: 10.add_new_sample
PDPSimple::announceParticipantState函数调用
a.PDP::announceParticipantState函数
b.StatelessWriter的unsent_changes_reset 如果没有新change,就发送history中之前的change 见9
-
PDP::announceParticipantState函数 主要干了2件事情
a.创建并初始化一个change 见6 将Participant的信息写入change
b.将change放入到WriterHistory中去 见3
-
RTPSWriter::new_change 创建了一个change,并配置相关参数
-
WriterHistory::add_change 调用WriterHistory::add_change_
-
WriterHistory::add_change_干了2件事
a.调用WriterHistory::prepare_and_add_change 见5
b.调用WriterHistory::notify_writer 见6
-
WriterHistory::prepare_and_add_change 给每个新change 一个sequenceNumber,然后将change存入
-
WriterHistory::notify_writer 调用statelesswriter 的 unsent_change_added_to_history函数
-
statelesswriter 的 unsent_change_added_to_history函数 调用FlowControllerimpl的add_new_sample
-
FlowControllerimpl的add_new_sample,将change 交给FlowControllerimpl 来发送出去
-
StatelessWriter的unsent_changes_reset, 就是发送history中的change
-
FlowControllerimpl的add_new_sample,将change 交给FlowControllerimpl 来发送出去
2.2源码解析
步骤1:
void PDP::announceParticipantState(
RTPSWriter& writer,
WriterHistory& history,
bool new_change,
bool dispose,
WriteParams& wparams)
{
if (enabled_)
{
// EPROSIMA_LOG_INFO(RTPS_PDP, "Announcing RTPSParticipant State (new change: " << new_change << ")");
CacheChange_t* change = nullptr;
if (!dispose)
{
//如果本地participant 的信息,有变化,则更新本地的ParticipantProxyData
if (m_hasChangedLocalPDP.exchange(false) || new_change)
{
this->mp_mutex->lock();
ParticipantProxyData* local_participant_data = getLocalParticipantProxyData();
InstanceHandle_t key = local_participant_data->m_key;
ParticipantProxyData proxy_data_copy(*local_participant_data);
this->mp_mutex->unlock();
if (history.getHistorySize() > 0)
{
//本地的ParticipantProxyData,都是在history的最上面
history.remove_min_change();
}
uint32_t cdr_size = proxy_data_copy.get_serialized_size(true);
//新建一个change
change = writer.new_change(
[cdr_size]() -> uint32_t
{
return cdr_size;
},
ALIVE, key);
if (change != nullptr)
{
//change内存 和 aux_msg的内存相关联
CDRMessage_t aux_msg(change->serializedPayload);
#if __BIG_ENDIAN__
change->serializedPayload.encapsulation = (uint16_t)PL_CDR_BE;
aux_msg.msg_endian = BIGEND;
#else
change->serializedPayload.encapsulation = (uint16_t)PL_CDR_LE;
aux_msg.msg_endian = LITTLEEND;
#endif // if __BIG_ENDIAN__
//ParticipantProxyData的信息写入CDRMessage_t 的对象aux_msg
if (proxy_data_copy.writeToCDRMessage(&aux_msg, true))
{
change->serializedPayload.length = (uint16_t)aux_msg.length;
//将change 放入history 中
history.add_change(change, wparams);
}
······
}
}
}
else
{
this->mp_mutex->lock();
ParticipantProxyData proxy_data_copy(*getLocalParticipantProxyData());
this->mp_mutex->unlock();
if (history.getHistorySize() > 0)
{
history.remove_min_change();
}
uint32_t cdr_size = proxy_data_copy.get_serialized_size(true);
change = writer.new_change([cdr_size]() -> uint32_t
{
return cdr_size;
},
NOT_ALIVE_DISPOSED_UNREGISTERED, getLocalParticipantProxyData()->m_key);
if (change != nullptr)
{
CDRMessage_t aux_msg(change->serializedPayload);
#if __BIG_ENDIAN__
change->serializedPayload.encapsulation = (uint16_t)PL_CDR_BE;
aux_msg.msg_endian = BIGEND;
#else
change->serializedPayload.encapsulation = (uint16_t)PL_CDR_LE;
aux_msg.msg_endian = LITTLEEND;
#endif // if __BIG_ENDIAN__
if (proxy_data_copy.writeToCDRMessage(&aux_msg, true))
{
change->serializedPayload.length = (uint16_t)aux_msg.length;
history.add_change(change, wparams);
}
else
{
EPROSIMA_LOG_ERROR(RTPS_PDP, "Cannot serialize ParticipantProxyData.");
}
}
}
}
}
// 创建change,初始化,并存入history中去
主要干了2件事
1.初始化CacheChange_t 对象( 步骤2)
2.将change 存入history中去(步骤3)
CDRMessage_t中的buffer 和 change->serializedPayload中的data 地址是一样的。
change 的类型有4种
ALIVE,
NOT_ALIVE_DISPOSED, //就是writer告知reader,我要下线了,你把这个消息处理一下
NOT_ALIVE_UNREGISTERED, //就是writer告知reader,我要把这个instance 注销掉
NOT_ALIVE_DISPOSED_UNREGISTERED//就是writer告知reader,我要下线你把消息处理一下,把这个instance 注销掉
步骤2:
CacheChange_t* RTPSWriter::new_change(
const std::function<uint32_t()>& dataCdrSerializedSize,
ChangeKind_t changeKind,
InstanceHandle_t handle)
{
EPROSIMA_LOG_INFO(RTPS_WRITER, "Creating new change");
std::lock_guard<RecursiveTimedMutex> guard(mp_mutex);
CacheChange_t* reserved_change = nullptr;
if (!change_pool_->reserve_cache(reserved_change))
······
uint32_t payload_size = fixed_payload_size_ ? fixed_payload_size_ : dataCdrSerializedSize();
if (!payload_pool_->get_payload(payload_size, *reserved_change))
······
reserved_change->kind = changeKind;
if (m_att.topicKind == WITH_KEY && !handle.isDefined())
{
EPROSIMA_LOG_WARNING(RTPS_WRITER, "Changes in KEYED Writers need a valid instanceHandle");
}
reserved_change->instanceHandle = handle;
reserved_change->writerGUID = m_guid;
reserved_change->writer_info.previous = nullptr;
reserved_change->writer_info.next = nullptr;
reserved_change->writer_info.num_sent_submessages = 0;
return reserved_change;
}
这个函数主要主要是从缓存中获取一个change,同时初始化change
change的缓存:
change_pool_ 和 payload_pool_这儿有两个缓存池
CacheChange_t 中有一个参数 SerializedPayload_t serializedPayload 这个对象是payload_pool_ 这个缓存池中缓存的对象
两个缓存池是关联的
change_pool是change的缓存池,而change中的serializedPayload是payload_pool_这个缓存池分配的。
缓存的类型有以下几种
PREALLOCATED_MEMORY_MODE, 提前分配好内存,那个内存按照最大值分配,这种分配方式,内存占用最高,但是需要分配的次数最少
PREALLOCATED_WITH_REALLOC_MEMORY_MODE,提前分配好内存,内存按照平均水平分配,如果来了一个消息,内存值大于之前分配的内存值,需要重新分配,这种分配方式,会比第一中需要的内存少点,但是分配的次数会增多
DYNAMIC_RESERVE_MEMORY_MODE, 动态分配内存,来一个重新分配一个,最少的内存占用,最多的内存分配次数
DYNAMIC_REUSABLE_MEMORY_MODE 动态分配内存,和上一个策略类似,会复用之前用过的内存,比上一个策略内存分配少一点,内存使用多一点
步骤3:
bool WriterHistory::add_change(
CacheChange_t* a_change,
WriteParams& wparams)
{
return add_change_(a_change, wparams);
}
ChangeKind_t 这个是RTPS消息在History中的存储的数据结构。
这个函数主要调用了add_change_ 函数 (步骤4)
将change 加入的history中
WriterHistory存放了所有需要发送的消息
步骤4:
bool WriterHistory::add_change_(
CacheChange_t* a_change,
WriteParams& wparams,
std::chrono::time_point<std::chrono::steady_clock> max_blocking_time)
{
if (mp_writer == nullptr || mp_mutex == nullptr)
{
EPROSIMA_LOG_ERROR(RTPS_WRITER_HISTORY,
"You need to create a Writer with this History before adding any changes");
return false;
}
std::lock_guard<RecursiveTimedMutex> guard(*mp_mutex);
if (!prepare_and_add_change(a_change, wparams))
{
return false;
}
notify_writer(a_change, max_blocking_time);
return true;
}
主要干了2件事情:
1.prepare_and_add_change
也就是步骤5
2.notify_writer 也就是步骤6
将change 放入 WriterHistory中
notify_writer主要是告知writer
步骤5:
bool WriterHistory::prepare_and_add_change(
CacheChange_t* a_change,
WriteParams& wparams)
{
------
++m_lastCacheChangeSeqNum;
//每个change有一个唯一的sequenceNumber,是由WriterHistory的m_lastCacheChangeSeqNum 赋值的
a_change->sequenceNumber = m_lastCacheChangeSeqNum;
if (wparams.source_timestamp().seconds() < 0)
{
Time_t::now(a_change->sourceTimestamp);
}
else
{
a_change->sourceTimestamp = wparams.source_timestamp();
}
a_change->writer_info.num_sent_submessages = 0;
a_change->write_params = wparams;
//Updated sample and related sample identities on the user's write params
wparams.sample_identity().writer_guid(a_change->writerGUID);
wparams.sample_identity().sequence_number(a_change->sequenceNumber);
//这个能够帮助回复的消息 和 请求的消息相关联
wparams.related_sample_identity(wparams.sample_identity());
set_fragments(a_change);
m_changes.push_back(a_change);
if (static_cast<int32_t>(m_changes.size()) == m_att.maximumReservedCaches)
{
m_isHistoryFull = true;
}
······
return true;
}
这个函数干了2件事
1.设置change的时间戳和sequenceNumber
2.set_fragments 如果change 数据量太大,就将change 分片
先看设置change的时间戳和sequenceNumber
每个change有一个唯一的sequenceNumber,是由WriterHistory的m_lastCacheChangeSeqNum(简称seq) 赋值的,这是change的唯一标志
这个标识很重要,在消息由于某种原因丢失重发的机制中发挥重要作用。我们发送消息的时候,会告知writer,我们要发送的message的seq是多少,我们丢失了消息之后,reader给writer发送心跳消息的时候,需要告诉writer哪个seq的消息丢失了,writer就重发相应的消息,整个机制比较复杂,我们在后面详细介绍。
时间戳的话就是将change 的时间戳设置成现在的系统时间
步骤6:
void WriterHistory::notify_writer(
CacheChange_t* a_change,
const std::chrono::time_point<std::chrono::steady_clock>& max_blocking_time)
{
mp_writer->unsent_change_added_to_history(a_change, max_blocking_time);
}
调用writer的unsent_change_added_to_history函数,其实就是将change 发送出去
步骤7:
void StatelessWriter::unsent_change_added_to_history(
CacheChange_t* change,
const std::chrono::time_point<std::chrono::steady_clock>& max_blocking_time)
{
std::lock_guard<RecursiveTimedMutex> guard(mp_mutex);
auto payload_length = change->serializedPayload.length;
if (liveliness_lease_duration_ < c_TimeInfinite)
{
// 发送liveness的包
mp_RTPSParticipant->wlp()->assert_liveliness(
getGuid(),
liveliness_kind_,
liveliness_lease_duration_);
}
// Notify the datasharing readers
// This also prepares the metadata for late-joiners
if (is_datasharing_compatible())
{
datasharing_delivery(change);
}
// Now for the rest of readers
if (!fixed_locators_.empty() || getMatchedReadersSize() > 0)
{
//只有在fixed_locators_不为空或者getMatchedReadersSize()>0的时候才会调用这个函数
flow_controller_->add_new_sample(this, change, max_blocking_time);
}
else
{
EPROSIMA_LOG_INFO(RTPS_WRITER, "No reader to add change.");
if (mp_listener != nullptr)
{
mp_listener->onWriterChangeReceivedByAll(this, change);
}
}
// Throughput should be notified even if no matches are available
on_publish_throughput(payload_length);
}
主要干了这几件事情:
1.wlp()->assert_liveliness 这个在wlp章节介绍详细内容,
主要是改变本地writer 的状态,因为本地writer有了发送动作,表明本地的writer是live的状态
2.当writer 知道WriterHistory中有新的消息
调用datasharing_delivery 就先发送本地的消息,(同进程,跨进程消息)
(我不推荐dds的跨进程通信
并不是dds的跨进程通信写的不好,而是跨进程通信对于dds来说就是一个鸡肋,如果使用跨进程通信,完全没必要使用dds 这么重的方式,写一个纯净的跨进程通信的代码就可以。)
3.flow_controller*->add_new_sample这个函数就是将消息放入flow_controller*中,这个之前介绍过
flow_controller就是控制消息发送的控制类
这个是有条件的就是需要fixed_locators不为空,这个fixed_locators之前说过就是intialPeersList经过处理后的数据
发送跨设备消息
步骤8:
bool add_new_sample(
fastrtps::rtps::RTPSWriter* writer,
fastrtps::rtps::CacheChange_t* change,
const std::chrono::time_point<std::chrono::steady_clock>& max_blocking_time) override
{
return add_new_sample_impl(writer, change, max_blocking_time);
}
步骤9:
void StatelessWriter::unsent_changes_reset()
{
std::lock_guard<RecursiveTimedMutex> guard(mp_mutex);
std::for_each(mp_history->changesBegin(), mp_history->changesEnd(), [&](CacheChange_t* change)
{
flow_controller_->add_new_sample(this, change,
std::chrono::steady_clock::now() + std::chrono::hours(24));
});
}
将history中的消息通过flow_controller 发送出去
步骤10:
和步骤8 一样
两个发送动作是一样的,就是发送的locator是不一样的
步骤8中发送的其实是已经配对的reader(包括initialpeerlist),步骤10发送的history中的历史消息
在第7步中我们看到干了3件事 1.wlp()->assert_liveliness 2.调用datasharing_delivery 就先发送本地的消息,(同进程,跨进程消息) 3.flow_controller*->add_new_sample 发送跨设备消息 我们下一篇先介绍 发送跨设备消息,然后再介绍发送本地消息和assert_liveliness
车载消息中间件FastDDS 源码解析(一)FastDDS 介绍和使用
车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上)
车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)
车载消息中间件FastDDS 源码解析(四)RtpsParticipant的创建(下)
车载消息中间件FastDDS 源码解析(五)BuiltinProtocols(上)
车载消息中间件FastDDS 源码解析(六)BuiltinProtocols(中)EDP
车载消息中间件FastDDS 源码解析(七)BuiltinProtocols(下)WLP&TypeLookupManager