车载消息中间件FastDDS 源码解析(一)FastDDS 介绍和使用
车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上)
车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)
车载消息中间件FastDDS 源码解析(四)RtpsParticipant的创建(下)
车载消息中间件FastDDS 源码解析(五)BuiltinProtocols(上)
车载消息中间件FastDDS 源码解析(六)BuiltinProtocols(中)EDP
车载消息中间件FastDDS 源码解析(七)BuiltinProtocols(下)WLP&TypeLookupManager
车载消息中间件FastDDS 源码解析(八)TimedEvent
车载消息中间件FastDDS 源码解析(十)发送第一条PDP消息(上)
FastDDS 源码解析(十二)发送第一条PDP消息(下)---异步发送
FastDDS 源码解析(十三)发送第一条PDP消息---跨进程发送
上一篇我们介绍了一条pdp消息的大概内容,和接收到pdp消息之后到分发给statelessreader处理的大概过程,这一篇我们介绍一下statelessreader如何接收这条消息
1.StatelessReader对于消息的处理
1.1类图
classDiagram
MessageReceiver *-- StatelessReader
ReaderListener <|-- PDPListener
StatelessReader *-- PDPListener
StatelessReader *-- ReaderHistory
class MessageReceiver{
+std::unordered_map<EntityId_t, std::vector<RTPSReader*>> associated_readers_
}
class StatelessReader{
+ReaderHistory* mp_history
+ReaderListener* mp_listener
}
1.MessageReceiver 有一个RTPSReader的队列
StatelessReader是RTPSReader的子类,MessageReceiver收到消息后交给RTPSReader处理
2.StatelessReader中有2个对象
ReaderHistory 和 ReaderListener,StatelessReader收到消息后,存入ReaderHistory,然后通过ReaderListener通知其他类处理
3.PDPListener是ReaderListener的子类
1.2时序图
sequenceDiagram
participant MessageReceiver
participant StatelessReader
participant ReaderHistory
participant PDPListener
participant ExternalLocatorsProcessor
MessageReceiver ->> StatelessReader: 1.processDataMsg()
StatelessReader ->> StatelessReader: 2.change_received()
StatelessReader ->>ReaderHistory:3.received_change()
StatelessReader ->>PDPListener:4.onNewCacheChangeAdded()
PDPListener ->> ExternalLocatorsProcessor:5.filter_remote_locators()
PDPListener ->> PDPSimple:6.assignRemoteEndpoints()
1.StatelessReader::processDataMsg 主要干了这几件事情
check一下消息能不能被接收,能被接收的话,看一下消息之前有没有被处理过(根据sequenceNumber),更新sequenceNumber,CacheChange_t 拷贝一份,交给change_received()处理
2.change_received 的主要干了2件事情
a.将CacheChange_t 交给 ReaderHistory的函数change_received()处理 见3
b.调用PDPListener::onNewCacheChangeAdded函数 见4
3.ReaderHistory的函数change_received() 调用add_change,将CacheChange_t 加入到ReaderHistory 中去
4.PDPListener::onNewCacheChangeAdded 主要干了这几件事情
a.参数校验,将CacheChange_t 的数据读入ParticipantProxyData
b.ExternalLocatorsProcessor 的filter_remote_locators 函数
c.PDPSimple 的assignRemoteEndpoints 参数是ParticipantProxyData
5.ExternalLocatorsProcessor 的filter_remote_locators 函数 过滤 remote_locators, 保留一个新的remote_locator
6.PDPSimple 的assignRemoteEndpoints
bool StatelessReader::processDataMsg(
CacheChange_t* change)
{
assert(change);
std::unique_lock<RecursiveTimedMutex> lock(mp_mutex);
//check 一下消息是不是能够被接收
if (acceptMsgFrom(change->writerGUID, change->kind))
{
// Always assert liveliness on scope exit
// wlp相关内容
auto assert_liveliness_lambda = [&lock, this, change](void*)
{
lock.unlock(); // Avoid deadlock with LivelinessManager.
assert_writer_liveliness(change->writerGUID);
};
std::unique_ptr<void, decltype(assert_liveliness_lambda)> p{ this, assert_liveliness_lambda };
------
// Check rejection by history
// check一下这个message之前是否被接收过
if (!thereIsUpperRecordOf(change->writerGUID, change->sequenceNumber))
{
bool will_never_be_accepted = false;
//check一下mp_history 能否装下change
if (!mp_history->can_change_be_added_nts(change->writerGUID, change->serializedPayload.length, 0,
will_never_be_accepted))
{
if (will_never_be_accepted)
{ //更新最后收到的消息number,这是为了后续统计消息的接收和缺失情况
update_last_notified(change->writerGUID, change->sequenceNumber);
}
return false;
}
//接收到不是自己reader的消息,更新最后收到的消息number,返回true
//接收到特定reader的消息,就往下走
if (data_filter_ && !data_filter_->is_relevant(*change, m_guid))
{
update_last_notified(change->writerGUID, change->sequenceNumber);
return true;
}
// Ask the pool for a cache change
CacheChange_t* change_to_add = nullptr;
// 分配一个
// change_pool 和payloadpool不是一个pool
if (!change_pool_->reserve_cache(change_to_add))
{
EPROSIMA_LOG_WARNING(RTPS_MSG_IN,
IDSTRING "Reached the maximum number of samples allowed by this reader's QoS. Rejecting change for reader: " <<
m_guid );
return false;
}
// Copy metadata to reserved change
change_to_add->copy_not_memcpy(change);
// Ask payload pool to copy the payload
IPayloadPool* payload_owner = change->payload_owner();
// 看一下是否是可以通过是能通过跨进程访问到
bool is_datasharing = std::any_of(matched_writers_.begin(), matched_writers_.end(),
[&change](const RemoteWriterInfo_t& writer)
{
return (writer.guid == change->writerGUID) && (writer.is_datasharing);
});
if (is_datasharing)
{
//We may receive the change from the listener (with owner a ReaderPool) or intraprocess (with owner a WriterPool)
ReaderPool* datasharing_pool = dynamic_cast<ReaderPool*>(payload_owner);
if (!datasharing_pool)
{
datasharing_pool = datasharing_listener_->get_pool_for_writer(change->writerGUID).get();
}
if (!datasharing_pool)
{
EPROSIMA_LOG_WARNING(RTPS_MSG_IN, IDSTRING "Problem copying DataSharing CacheChange from writer "
<< change->writerGUID);
change_pool_->release_cache(change_to_add);
return false;
}
datasharing_pool->get_payload(change->serializedPayload, payload_owner, *change_to_add);
}
// 给change_to_add 分配payload
else if (payload_pool_->get_payload(change->serializedPayload, payload_owner, *change_to_add))
{
change->payload_owner(payload_owner);
}
else
{
EPROSIMA_LOG_WARNING(RTPS_MSG_IN, IDSTRING "Problem copying CacheChange, received data is: "
<< change->serializedPayload.length << " bytes and max size in reader "
<< m_guid << " is "
<< (fixed_payload_size_ > 0 ? fixed_payload_size_ : std::numeric_limits<uint32_t>::max()));
change_pool_->release_cache(change_to_add);
return false;
}
// Perform reception of cache change
if (!change_received(change_to_add))
{
EPROSIMA_LOG_INFO(RTPS_MSG_IN,
IDSTRING "MessageReceiver not add change " << change_to_add->sequenceNumber);
change_to_add->payload_owner()->release_payload(*change_to_add);
change_pool_->release_cache(change_to_add);
return false;
}
}
}
return true;
}
//这个函数主要是对data消息的处理
1.check一下这个消息是不是能够被接收
2.check一下这个消息是不是被接收过,如果被接收过就不处理
3.如果没有被接收过,将change,加入到history中去(如果能够跨进程共享,则change的存储空间从共享内存中分配,如果不能进程间共享,则从其他地方分配)
4.调用change_received 见步骤2
步骤2:
bool StatelessReader::change_received(
CacheChange_t* change)
{
// Only make the change visible if there is not another with a bigger sequence number.
// TODO Revisar si no hay que incluirlo.
// 是不是已经收到
if (!thereIsUpperRecordOf(change->writerGUID, change->sequenceNumber))
{
// Update Ownership strength.
// 所有权强度
if (EXCLUSIVE_OWNERSHIP_QOS == m_att.ownershipKind)
{
auto writer = std::find_if(matched_writers_.begin(), matched_writers_.end(),
[change](const RemoteWriterInfo_t& item)
{
return item.guid == change->writerGUID;
});
assert(matched_writers_.end() != writer);
change->reader_info.writer_ownership_strength = writer->ownership_strength;
}
else
{
change->reader_info.writer_ownership_strength = std::numeric_limits<uint32_t>::max();
}
if (mp_history->received_change(change, 0))
{
auto payload_length = change->serializedPayload.length;
auto guid = change->writerGUID;
auto seq = change->sequenceNumber;
Time_t::now(change->reader_info.receptionTimestamp);
SequenceNumber_t previous_seq = update_last_notified(change->writerGUID, change->sequenceNumber);
++total_unread_;
//空函数
on_data_notify(guid, change->sourceTimestamp);
//PDPListener
auto listener = getListener();
if (listener != nullptr)
{
if (SequenceNumber_t{0, 0} != previous_seq)
{
assert(previous_seq < seq);
uint64_t tmp = (seq - previous_seq).to64long() - 1;
int32_t lost_samples = tmp > static_cast<uint64_t>(std::numeric_limits<int32_t>::max()) ?
std::numeric_limits<int32_t>::max() : static_cast<int32_t>(tmp);
if (0 < lost_samples) // There are lost samples.
{
//消息丢失做相关处理
//这儿是个空函数
listener->on_sample_lost(this, lost_samples);
}
}
// WARNING! These methods could destroy the change
bool notify_single = false;
// 当change available的时候,调用
// 这里将 notify_single变为true
listener->on_data_available(this, guid, seq, seq, notify_single);
if (notify_single)
{
//NewCacheChange add
listener->onNewCacheChangeAdded(this, change);
}
}
//这个是多线程通知,有线程在等待新消息,通知这个线程有新消息到来,读取新消息
new_notification_cv_.notify_all();
// statistics callback
on_subscribe_throughput(payload_length);
return true;
}
}
return false;
}
statelessReader 的这个函数中很多逻辑其实没什么用
41-53行,statelessReader没有状态也不保存接收的数据,所以不存在所谓的数据丢失问题
56-59行,statelessReader没有必要调用on_data_available,这是个空函数
67行 statelessreader 也没有需要等待的线程
整体来看StatelessReader::change_received 有比较多的冗余代码,主要是干了2件事:
1.ReaderHistory::received_change 步骤3
2.调用了listener->onNewCacheChangeAdded这个函数,调用到了PDPListener的onNewCacheChangeAdded
也就是步骤4
我们看到这块其实有大量无用代码,可能为以后的代码预埋,逻辑上说其实没有必要。
步骤3:
bool ReaderHistory::received_change(
CacheChange_t* change,
size_t)
{
return add_change(change);
}
bool ReaderHistory::add_change(
CacheChange_t* a_change)
{
------
eprosima::utilities::collections::sorted_vector_insert(m_changes, a_change, fastdds::rtps::history_order_cmp);
------
return true;
}
将数据按照时间顺序存入ReaderHistory
步骤4:
void PDPListener::onNewCacheChangeAdded(
RTPSReader* reader,
const CacheChange_t* const change_in)
{
CacheChange_t* change = const_cast<CacheChange_t*>(change_in);
//远端的id
GUID_t writer_guid = change->writerGUID;
EPROSIMA_LOG_INFO(RTPS_PDP, "SPDP Message received from: " << change_in->writerGUID);
// Make sure we have an instance handle (i.e GUID)
if (change->instanceHandle == c_InstanceHandle_Unknown)
{
//如果没有guid,从mp_PDPReaderHistory 中移除
if (!this->get_key(change))
{
EPROSIMA_LOG_WARNING(RTPS_PDP, "Problem getting the key of the change, removing");
parent_pdp_->builtin_endpoints_->remove_from_pdp_reader_history(change);
return;
}
}
// Take GUID from instance handle
GUID_t guid;
iHandle2GUID(guid, change->instanceHandle);
//如果alive 处理change的信息
if (change->kind == ALIVE)
{
// Ignore announcement from own RTPSParticipant
// 如果 change是自己的participant发出的,就不做处理将change 移除
if (guid == parent_pdp_->getRTPSParticipant()->getGuid())
{
EPROSIMA_LOG_INFO(RTPS_PDP, "Message from own RTPSParticipant, removing");
parent_pdp_->builtin_endpoints_->remove_from_pdp_reader_history(change);
return;
}
// Release reader lock to avoid ABBA lock. PDP mutex should always be first.
// Keep change information on local variables to check consistency later
SequenceNumber_t seq_num = change->sequenceNumber;
reader->getMutex().unlock();
std::unique_lock<std::recursive_mutex> lock(*parent_pdp_->getMutex());
reader->getMutex().lock();
// If change is not consistent, it will be processed on the thread that has overriten it
if ((ALIVE != change->kind) || (seq_num != change->sequenceNumber) || (writer_guid != change->writerGUID))
{
return;
}
// Access to temp_participant_data_ is protected by reader lock
// Load information on temp_participant_data_
CDRMessage_t msg(change->serializedPayload);
temp_participant_data_.clear();
//解析msg消息,放入temp_participant_data_
if (temp_participant_data_.readFromCDRMessage(&msg, true, parent_pdp_->getRTPSParticipant()->network_factory(),
parent_pdp_->getRTPSParticipant()->has_shm_transport()))
{
// After correctly reading it
change->instanceHandle = temp_participant_data_.m_key;
guid = temp_participant_data_.m_guid;
if (parent_pdp_->getRTPSParticipant()->is_participant_ignored(guid.guidPrefix))
{
return;
}
// Filter locators
const auto& pattr = parent_pdp_->getRTPSParticipant()->getAttributes();
// 过滤
// 一个新加的功能,将temp_participant_data_中的ip地址进行过滤
fastdds::rtps::ExternalLocatorsProcessor::filter_remote_locators(temp_participant_data_,
pattr.builtin.metatraffic_external_unicast_locators, pattr.default_external_unicast_locators,
pattr.ignore_non_matching_locators);
// Check if participant already exists (updated info)
ParticipantProxyData* pdata = nullptr;
for (ParticipantProxyData* it : parent_pdp_->participant_proxies_)
{
if (guid == it->m_guid)
{
pdata = it;
break;
}
}
// pdata == nullptr 就是新发现的Participant,否则CHANGED_QOS_PARTICIPANT 更新信息
auto status = (pdata == nullptr) ? ParticipantDiscoveryInfo::DISCOVERED_PARTICIPANT :
ParticipantDiscoveryInfo::CHANGED_QOS_PARTICIPANT;
if (pdata == nullptr)
{
// Create a new one when not found
pdata = parent_pdp_->createParticipantProxyData(temp_participant_data_, writer_guid);
reader->getMutex().unlock();
lock.unlock();
if (pdata != nullptr)
{
------
RTPSParticipantListener* listener = parent_pdp_->getRTPSParticipant()->getListener();
if (listener != nullptr)
{
bool should_be_ignored = false;
{
std::lock_guard<std::mutex> cb_lock(parent_pdp_->callback_mtx_);
ParticipantDiscoveryInfo info(*pdata);
info.status = status;
//
listener->onParticipantDiscovery(
parent_pdp_->getRTPSParticipant()->getUserRTPSParticipant(),
std::move(info),
should_be_ignored);
}
if (should_be_ignored)
{
parent_pdp_->getRTPSParticipant()->ignore_participant(guid.guidPrefix);
}
}
// Assigning remote endpoints implies sending a DATA(p) to all matched and fixed readers, since
// StatelessWriter::matched_reader_add marks the entire history as unsent if the added reader's
// durability is bigger or equal to TRANSIENT_LOCAL_DURABILITY_QOS (TRANSIENT_LOCAL or TRANSIENT),
// which is the case of ENTITYID_BUILTIN_SDP_PARTICIPANT_READER (TRANSIENT_LOCAL). If a remote
// participant is discovered before creating the first DATA(p) change (which happens at the end of
// BuiltinProtocols::initBuiltinProtocols), then StatelessWriter::matched_reader_add ends up marking
// no changes as unsent (since the history is empty), which is OK because this can only happen if a
// participant is discovered in the middle of BuiltinProtocols::initBuiltinProtocols, which will
// create the first DATA(p) upon finishing, thus triggering the sent to all fixed and matched
// readers anyways.
parent_pdp_->assignRemoteEndpoints(pdata);
}
}
else
{
pdata->updateData(temp_participant_data_);
pdata->isAlive = true;
reader->getMutex().unlock();
······
if (parent_pdp_->updateInfoMatchesEDP())
{
parent_pdp_->mp_EDP->assignRemoteEndpoints(*pdata);
}
lock.unlock();
RTPSParticipantListener* listener = parent_pdp_->getRTPSParticipant()->getListener();
if (listener != nullptr)
{
bool should_be_ignored = false;
{
std::lock_guard<std::mutex> cb_lock(parent_pdp_->callback_mtx_);
ParticipantDiscoveryInfo info(*pdata);
info.status = status;
listener->onParticipantDiscovery(
parent_pdp_->getRTPSParticipant()->getUserRTPSParticipant(),
std::move(info),
should_be_ignored);
}
if (should_be_ignored)
{
parent_pdp_->getRTPSParticipant()->ignore_participant(temp_participant_data_.m_guid.guidPrefix);
}
}
}
// Take again the reader lock
reader->getMutex().lock();
}
}
else
{
reader->getMutex().unlock();
if (parent_pdp_->remove_remote_participant(guid, ParticipantDiscoveryInfo::REMOVED_PARTICIPANT))
{
reader->getMutex().lock();
// All changes related with this participant have been removed from history by remove_remote_participant
return;
}
reader->getMutex().lock();
}
//Remove change form history.
parent_pdp_->builtin_endpoints_->remove_from_pdp_reader_history(change);
}
change 的类型有4种
ALIVE,
NOT_ALIVE_DISPOSED, //就是writer告知reader,我要下线了,你把这个消息处理一下
NOT_ALIVE_UNREGISTERED, //就是writer告知reader,我要把这个instance 注销掉
NOT_ALIVE_DISPOSED_UNREGISTERED//就是writer告知reader,我要下线你把消息处理一下,把这个instance 注销掉
如果不是ALIVE就意味着对端的Participant已经下线,调用parent_pdp_->remove_remote_participant
PDP消息本身包含了这个Participant的主要的信息,从消息中读取信息存入ParticipantProxyData 对象,读取了信息之后我们看一下,这个participant 是否已经存在,是否需要被忽略,已经存在那么更新这个Participant的信息,如果不存在新建一个ParticipantProxyData
上面函数主要做了这几件事
1.判断 change的类型是否是ALIVE,不是ALIVE表示对端的Participant已经下线,调用parent_pdp_->remove_remote_participant
是ALIVE则到步骤2
2.从change中读取信息数据,存入ParticipantProxyData
3.调用fastdds::rtps::ExternalLocatorsProcessor::filter_remote_locators 这个在第三部分介绍
4.判断这个participant 是否已经存在,如果存在更新这个Participant的ParticipantProxyData,不存在则新建ParticipantProxyData
5.调用PDPSimple 的assignRemoteEndpoints 这个会在下一篇详细介绍
主要功能就是Participant匹配后,根据Participant的信息匹配PDP中的 StatelessWriter和StatelessReader,EDP中的众多Writer和Reader,WLP中的众多Writer和Reader。
2.0一个功能彩蛋
fastdds::rtps::ExternalLocatorsProcessor::filter_remote_locators
这是fastdds 最近新增的一个功能,需要用户在程序中进行配置,metatraffic_external_unicast_locators,default_external_locators
对于这些配置的ip进行了分级。
举个例子,我们的一个房间里租了一个局域网,然后整个楼层又组了一个局域网。
那么房间中各个设备的ip是一个层级的,楼层的ip是另一个层级的。
metatraffic_external_unicast_locators,default_external_locators 中配置了ip,子网掩码,以及这个ip所在的层级。
然后我们根据这些配置对ParticipantProxyData中的ip地址进行过滤,只留下能够通信的ip地址。
还是以我们手机举例,有多个地址(可以参考车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)中3.2节),有wifi p2p地址,局域网地址,公共网地址,那么可以在这儿做设置,这样就可以过滤掉不能通信的地址。
过滤远端的ParticipantProxyData中的ip地址
void filter_remote_locators(
ParticipantProxyData& data,
const ExternalLocators& metatraffic_external_locators,
const ExternalLocators& default_external_locators,
bool ignore_non_matching)
{
// 这儿是Participant自己的metatraffic_locators.unicast
filter_remote_locators(data.metatraffic_locators.unicast, metatraffic_external_locators, ignore_non_matching);
// 这儿是Participant自己的default_locators.unicast
filter_remote_locators(data.default_locators.unicast, default_external_locators, ignore_non_matching);
}
这里是具体的算法
static void filter_remote_locators(
fastrtps::ResourceLimitedVector<Locator>& locators,
const ExternalLocators& external_locators,
bool ignore_non_matching)
{
auto compare_locators = [external_locators, ignore_non_matching](const Locator& lhs, const Locator& rhs) -> bool
{
return heuristic(lhs, external_locators, ignore_non_matching) <
heuristic(rhs, external_locators, ignore_non_matching);
};
/* This will sort the received locators according to the following criteria:
* 1. Non-matching locators when not ignored. Heuristic value: 0
* 2. Matching locators. Heuristic value: ((255ull - externality) << 16) | (cost << 8)
* 3. Non-matching locators when ignored. Heuristic value: max_uint64_t
*
* The heuristic has been chosen so non-matching locators will never give a value that will be given to a matching
* locator. Matching locators will be sorted first by highest externality, then by lowest cost.
*/
// 从小到大排列
std::sort(locators.begin(), locators.end(), compare_locators);
/* Remove non-matching locators if requested to.
* This is done by removing all locators at the end with an heuristic value of max_uint64_t.
*/
if (ignore_non_matching)
{
while (!locators.empty())
{
uint64_t h = heuristic(locators.back(), external_locators, ignore_non_matching);
if (std::numeric_limits<uint64_t>::max() != h)
{
break;
}
//如果没有找到匹配的就去除
locators.pop_back();
}
}
// Check what locators to keep
auto it = locators.begin();
// Keep non-matching locators with an heuristic value of 0.
if (!ignore_non_matching)
{
//没有匹配的情况下,为0
while (it != locators.end() && (0 == heuristic(*it, external_locators, ignore_non_matching)))
{
++it;
}
}
it不会是shm的情况,从local开始
// Traverse external_locators in heuristic order, checking if certain heuristic value should be ignored
// external_locators index从高到低遍历
for (const auto& externality : external_locators)
{
for (const auto& cost : externality.second)
{
// Check if the locators on this heuristic value should be ignored
//externality.first 是index,cost.first是cost
uint64_t entry_heuristic = heuristic_value(externality.first, cost.first);
auto end_it = it;
size_t num_exactly_matched = 0;
while (end_it != locators.end() &&
(entry_heuristic == heuristic(*end_it, external_locators, ignore_non_matching)))
{
for (const LocatorWithMask& local_locator : cost.second)
{
if (std::equal(end_it->address, end_it->address + 16, local_locator.address))
{
// 地址相等
++num_exactly_matched;
break;
}
}
++end_it;
}
if (end_it != it)
{
// There was at least one locator with this heuristic value
if (externality.first > 0 &&
num_exactly_matched == cost.second.size() &&
end_it != locators.end() &&
static_cast<size_t>(std::distance(it, end_it)) == num_exactly_matched)
{
// All locators on this heuristic were the local locators, ignore this heuristic
// 值一样
it = locators.erase(it, end_it);
}
else
{
// We should keep this locators, remove the rest and return
// 保持一个locator ,这个locator 的cost 或者level 有变化
it = locators.erase(end_it, locators.end());
return;
}
}
}
}
}
//从大到小排列
ExternalLocators = std::map<
uint8_t, // externality_index
std::map<
uint8_t, // cost
std::vector<LocatorWithMask> // locators with their mask
>,
std::greater<uint8_t> // Ordered by greater externality_index
>;
具体算法在这儿,主要是过滤了一些无用的ip,这个过滤需要在初始化的时候进行设置,才能起效。具体如何设置,我这边没有设置过,后续做了实验之后,再把相关结果贴出来。
这一篇我们介绍一下statelessreader如何处理这条消息,下一篇我们介绍一下收到pdp消息之后,PDP如何匹配。
车载消息中间件FastDDS 源码解析(一)FastDDS 介绍和使用
车载消息中间件FastDDS 源码解析(二)RtpsParticipant的创建(上)
车载消息中间件FastDDS 源码解析(三)RtpsParticipant的创建(中)
车载消息中间件FastDDS 源码解析(四)RtpsParticipant的创建(下)
车载消息中间件FastDDS 源码解析(五)BuiltinProtocols(上)
车载消息中间件FastDDS 源码解析(六)BuiltinProtocols(中)EDP
车载消息中间件FastDDS 源码解析(七)BuiltinProtocols(下)WLP&TypeLookupManager
车载消息中间件FastDDS 源码解析(八)TimedEvent
车载消息中间件FastDDS 源码解析(十)发送第一条PDP消息(上)
FastDDS 源码解析(十二)发送第一条PDP消息(下)---异步发送