百度Apollo系统学习-Cyber RT 通信-上层

简介

Cyber作为一个中间件,最基础的功能就是解决不同模块不同进程之间的通信问题,所以这篇文章主要带大家理清cyber中的通信方式。因为这部分内容较多,所以通信模块会分两篇文章来说明,本文为上篇,主要关注偏上层的部分(下图中除了Transport的部分)。
注:本文中部分图片来自@Kit Fung的Apollo分析博客
jiegou

通信类结构

node

第零层:

Node

上图展示了通信功能封装最顶层的类Node,每个Component都有一个Node,每个Node负责创建Reader, Writer, Service, Client来帮该组件获取信息或传达信息。每个Node保存了它的名字和名字空间,一个存有channel_name对应的Reader的map以及一个NodeChannelImpl和一个NodeServiceImpl(创建以上4种东西的创建器)。

第一层:

NodeChannelImpl

NodeChannelImplNode用来创建Channel相关的ReaderWriter的类,在实际环境中,创建的为ReaderWriter,虚拟环境中创建的是IntraReaderIntraWriter。它会向通信拓扑注册当前节点。

NodeServiceImpl

NodeChannelImpl类似,只不过它创建的是ServiceClient,还会注册service。

第二层:

Reader

Reader继承自ReaderBase,它包含一个Blocker和一个ReceiverReader只在实际环境中会被NodeChannelImpl创建,并主要做两个事:
- 每当该channel来了一个消息,就会调用其Enqueue函数,该函数会调用Blocker::Publish函数把该消息放入Reader中的Blockerpublished_msg_queue队列并调用Blocker保存的相应的回调函数来处理(可是实际环境中Blocker的回调函数为空,所以不会有用)。
- 如果一开始设置有一个回调函数,那么当message到达时就会在调用Enqueue函数后额外调用该回调函数来处理(可是实际环境中Reader的回调函数都为空,所以这一步是不可能发生的)

Writer

向对应的channel写数据,最后会调用到transport::Transmitter->Transmit函数,每个Writer只能写一个channel但某个channel可以有多个Writer

IntraReader

代码在cyber/bolcker/intra_reader.h,它继承自Reader,只在虚拟环境中会被NodeChannelImpl创建。它会重载Reader::Init函数,不再创建协程,而是直接把IntraReader::OnMessage(记录时间并调用IntraReader创建时传入的回调函数)注册为该Blocker的回调函数。

IntraWriter

同样是虚拟环境中用的,它的Write函数不会调用到Transmitter的方法,而是直接调用到Blocker::Publish函数来放入数据。

第三层:

第三层

Blocker & BlockerManager

BlockerReader的一个成员,BlockerManager保存了全局的一张channel_name对应Blocker的map。Blocker里面保存了一张callback_id和回调函数的map,我的理解是Blocker是一个消息的缓存类,同时也记录了该Reader的一些回调函数。这里之所以用一些是因为Blocker这个结构的主要功能应该是提供一个管理者获取数据的入口,方便debug、记录日志、运行虚拟环境和监控整个系统,所以在Blocker里注册的回调函数应该都是管理员注册的监控函数,和系统主逻辑没关系。

DataVisitor

  • 代码位于cyber/data/data_visitor.h,继承自DataVisitorBaseDataVisitorBase有一个指向DataNotifier的指针,还有一个回调函数Notifier,该回调函数在Component::Initialize函数最后调用Scheduler::CreateTaskcyber/scheduler/scheduler.cc中作为匿名函数被visitor->RegisterNotifyCallback注册)时被定义并赋值给DataVisitorBase中的notifier_(它的作用就是唤醒创建的协程,该协程做的事情就是调用dv->TryFetch来拿数据,如果成功就调用组件的Proc函数,具体在调度的博客里会解释)。该回调函数会在DataVisitor初始化的时候注册到全局的DataNotifier中的map中。
  • DataVisitorComponent::InitializeReader::Init中都会创建,是实际环境中获取数据的方式。它最终会调用到AllLatest::Fusion函数。每个DataVisitor的每个消息种类都会有一个缓存ChannelBuffer,在初始化的时候这些ChannelBuffer都会被加入到DataDispatcher中的一张map中。而所有种类的消息还会有一个消息整合体的ChannelBuffer,这条buffer不会放入全局map(因为不会有Dispatcher会直接发这种数据),但之后取数据都是从这里取。取的时候按照计数顺序也就是index来取,所以我们可以看到这里没有实时性保证(除非buffer队列长度为1,但系统默认长度其实也就是1,位于cyber/node/reader.h:DEFAULT_PENDING_QUEUE_SIZE)。
  • 对于有多个种类消息的DataVisitor还是只有一个Notifier对应M0,也就是说只有当M0消息来的时候协程会被唤醒并通过DataFusion来获取整合所有种类的信息。

DataNotifier

DataNotifier是一个全局的单例,保存有一个channel_id对应vector<std::shared_ptr<Notifier>>的map(即每个channel对应多个Notifier,因为有多个订阅者),在DataVisitor初始化的时候就会把自己的Notifier注册到这个map中。

DataDispatcher

  • DataDispatcher也是一个全局单例,记录了一个channel_id对应vector<std::weak_ptr<CacheBuffer<std::shared_ptr<T>>>>的map(也就是说每个channel会有好几个订阅者,每个订阅者拥有一条CacheBuffer,每次收到该channel的消息会给每个buffer都放一份)
  • 它主要的功能就是DataDispatcher::Dispatch函数用来向某个channel分发收到的数据,它会先从map中取出所有对应的buffer,然后调用CacheBuffer::Fill函数来给buffer填数据(这里的Fill函数还会去把所有种类的消息整合成一条新的整合数据,后面会介绍),之后再调用DataNotifier::Notify函数来找出所有对应该channel_idNotifier并调用它们唤醒一开始创建的协程来取数据并运行回调函数。

第四层

DataFusion & AllLatest

代码位于cyber/data/fusion/all_latest.hDataFusionDataVisitor中的一个成员,AllLatest实现了这个基类,它提供了Fusion函数用来拿所有种类数据的整合体。之所以会出现这样的一个方式是因为每次给最终的回调函数Proc处理数据时需要给出所有数据种类的一个整合,但不同数据产生的快慢不同,可能M0产生了1个但M1已经产生了10个了,这时候就需要从M1中挑一个最新的,旧的就直接丢弃了,可见***M0的选取非常重要***。而实现这个机制的方式是通过在Dispatch时实现的:
- 在初始化DataFusionAllLatest)时会给M0的ChannelBuffer设置一个回调函数,该回调函数会在每次有M0数据到来时(会调用CacheBuffer::Fill函数)被调用。普通的Fill函数会把收到的数据放入buffer,而有回调函数的CacheBuffer则不会这么做,而是只运行回调函数。
- 回调函数做了什么事呢。它会去M1,M2…这些其他种类的buffer中获取它们队列中最新的那条数据,然后把当前的M0和拿到的buffer中的M1,M2…这些整合成一个整体再放入新的队列。所以我们可以推测,一般M0都是获取最慢的那个数据类型,这样就能保证整体数据都是比较新的。
- 所以当Dispatch其他种类数据时,只是单纯往对应的buffer中填数据,当发送M0时会发生一次数据整合然后返回整合后的数据并唤醒Proc回调函数。

第五层

ChannelBuffer & CacheBuffer

实际上就是实现了一个队列,用来放置某个channel产生的数据。这里需要注意,每次有新数据产生,都会给每个等待着的队列复制一份并放入buffer(但内存是恒定的,因为buffer的长度一开始就定了)。
具体的队列实现在cyber/data/cache_buffer.h,很简单,有兴趣可以直接读代码。

不同环境下的数据流

因为需要提供虚拟环境,所以数据流变得非常混乱。这一部分的代码比较复杂,接下来我会带大家好好理解一下到底整个项目为这两个不同环境添加了多少tricky的内容。

实际环境 reality_mode

  • Component::Initialize函数是组件初始化的过程,在cyber/component/component.h中我们可以看到每个组件可以有0-4个消息分别对应4个不同的Initialize函数。我们看每个初始化函数,对于实际环境来说,初始化工作非常的明确,就是为每个消息类别创建一个Reader,然后创建回调函数(调用Process)并让调度器来运行该Task。
  • 实际环境中的主要回调函数就是每个组件类自己定义的Proc函数,它从DataVisitor中得到所有消息并运行。
  • 创建每个Reader时,Reader会调用自己的初始化函数(在cyber/node/reader.h),实际环境中的Reader创建都是不指定回调函数的,所以Reader创建的协程只会调用一下Reader::EnqueueBlocker::Subscribe只会在IntraReader::Init中用到,所以此处的Enqueue只起到了缓存消息的作用,Blocker::Notify函数等于没用)。
  • 总结:实际环境中,每个Component会创建一个协程来调用Proc回调函数,每个Component的每个Reader也会创建一个协程来缓存消息数据。Proc需要的所有消息都是通过DataVisitor获取的,不会再经过Reader

虚拟环境 !reality_mode

  • 虚拟环境和实际环境最大的差别就是数据来源不是真实传感器实时获取的数据了,所以最大的改变就是数据获取的方式变得更加简单(实际环境需要协调传感器的频率等等)。所以在虚拟环境中,所有的ReaderWriter都是IntraReaderIntraWriter,阅读器获取的数据不是其它组件的协程中生产的(Writer不会调到transport::Transmitter->Transmit),而是通过BlockerPublish函数来直接将模拟或历史数据放入缓存队列(所以可以“伪造”)。
  • 虚拟环境创建组件时可以看到,如果有n种消息,它会先给后n-1种消息创建IntraReader,然后对于M0消息,它会创建一个特殊的回调函数,该回调函数在接收到消息msg0时触发,触发时会从另外n-1个消息的IntraReaderBlocker的缓存队列中拿出另外n-1个消息,然后把这些消息一起交给ProcessProc函数来执行。这个回调函数会在创建IntraReader时当成参数传入并在IntraReader初始化时被注册到该IntraReaderBlocker中的回调函数map中。
  • 总结:虚拟环境中,每个Component不会创建协程,回调函数会在收到M0消息时被调用并且自动从其他的IntraReader中拿到数据,相比于实际环境中的数据获取更加简单,它不会用到DataVisitor

小结

可以看到,至此我们完成了通信模块偏上层部分的闭环。某数据从DataDispatcher::Dispatch开始被发送,然后向某个channel的所有等待的buffer中放入新来的数据并且通知所有以该数据为M0的DataVisitor,这些组件的协程被唤醒并被调度器调度运行,它尝试从缓存队列里取出M0以及其他种类数据的缓存数据,整合之后把整合后的数据放入回调函数运行。
可以看到,这个过程屏蔽了底层实现,那就是从Writer把消息写入之后如何被DataDispatcher::Dispatch所察觉的呢,在下一篇文章中我们会从底层入手来深入理解cyber的通信方式,填上这个缺口。

参考链接

Apollo 3.5 Cyber Blocker模塊簡單記錄
Apollo 3.5 Cyber data_visitor 分析

  • 9
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值