百度自动驾驶apollo源码解读22:/cyber/node模块和/cyber/service模块

文章详细解读了一个名为Node的类,它是Cyber框架中通信的基础。Node类包含NodeChannelImpl和NodeServiceImpl,分别用于实现基于通道的发布订阅和基于服务的一问一答通信模式。此外,还介绍了ReaderBase和Reader类、WriterBase和Writer类,它们是数据收发的基础。ServiceBase和服务类涉及服务的创建和交互,而ClientBase和Client类则处理服务请求和响应。文章探讨了这些组件的成员函数、属性以及它们在通信中的角色,特别提到了通信拓扑管理和数据处理的细节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

为什么要同时解读两个模块?因为:原作者把service单独领出来做了个模块,个人感觉没必要,此部分逻辑放在node模块之下更贴切。本篇文章做了统一解读。其通信结构如下图所示:
cyber通信结构

1.Node类

node类更像是一个代理类,值得注意的是他的构造函数是私有的,有四个友元,代表他实例化的四种方式,比较易懂不做解释了。他有node自己的基础属性,node_name_和name_space_,前者是代表节点名字,值得注意的是改名字要唯一,在统一局域网,或者统一台电脑上,后者默认为空,没发现有什么具体用处。两个主要成员NodeChannelImpl和NodeServiceImpl,我们知道cyber支持两种方式的通信,基于通道名字的发布订阅模式,和基于服务的一问一答模式,这两个成员是实现这两种通信模式的。主要成员函数为CreateReader、CreateWriter、CreateService、CreateClient(重载的不做考虑),两两一组分别完成上述两种通信模式。无论是通道模式还是服务模式,都是需要收发数据的,由此诞生两个基类Transmitter(发送者)和Receiver(接收者),两者都是派生于Endpoint,
都有四个子类,分别实现进程内部收发、不同进程之间收发、不同主机之间收发、不指定模式的收发,此处稍作介绍,以后详解。

2.NodeChannelImpl类

主要成员函数CreateReader、CreateWriter分别实例化一个Reader和Writer。

3.NodeServiceImpl

主要成员函数CreateService、CreateClient分别实例化一个Service<Request, Response>和ClientBase<Request, Response>。

4.ReaderBase类和Reader类

ReaderBase包含一系列的虚函数和两个成员role_attr_(通信成员属性)和init_(是否初始化)
Reader
latest_recv_time_sec_属性:上次接收到数据时间戳
second_to_lastest_recv_time_sec_属性:上上次接收到数据时间戳
pending_queue_size_属性:接收队列长度
reader_func_属性:读取数据之后回调函数
receiver_属性:用来接收数据的指针
croutine_name属性:_协程名字
blocker_属性:接收数据存放处
change_conn_属性:将channel的拓扑关系发生变化消息绑定到OnChannelChange放回的标注,此标注主要是为了离开拓扑关系
channel_manager_属性:channel拓扑关系管理器
JoinTheTopology接口:加入到channel拓扑关系
LeaveTheTopology接口:离开channel拓扑关系
OnChannelChange接口:channel拓扑关系发生变化响应函数
Enqueue接口:调用blocker_的push操作将消息加入到队列
Observe接口:干嘛的,还没搞懂
Init接口:初始化数据成员,创建协程,将读取到的数据调用Enqueue接口
Shutdown接口:Init接口的逆向
HasReceived接口:是否接收到过数据
Empty接口:数据队列是否为空
GetDelaySec接口:从名字上看像是收取的延迟,看实际实现是两贞之间的间隔
PendingQueueSize接口:数据队列的长度
GetLatestObserved接口:获取最后的消息
ClearData接口:清空缓存里面的数据
SetHistoryDepth接口:设置blocker_长度大小
GetHistoryDepth接口:获取blocker_长度大小
HasWriter接口:是否有读者
GetWriters接口:获取读者列表

5.WriterBase类和Writer类

WriterBase包含一系列的虚函数和两个成员role_attr_(通信成员属性)和init_(是否初始化),和一个锁lock_,这个锁是用来锁init_成员的,ReaderBase里面的init_是原子自变量所以不需要锁。泪奔,怎么兄弟类这还要做点小区别,感觉代码不是一个人写的,或者代码进行了迭代更新,ReaderBase忘了更新。
Writer类
transmitter_属性:用来发送数据的指针
change_conn_属性:将channel的拓扑关系发生变化消息绑定到OnChannelChange放回的标注,此标注主要是为了离开拓扑关系
channel_manager_属性:channel拓扑关系管理器
Init接口:实例化成员变量,加入到channel拓扑关系
Shutdown接口:Init接口的逆向
Write接口:利用transmitter_指针发送数据
JoinTheTopology接口:加入到channel拓扑关系
LeaveTheTopology接口:离开channel拓扑关系
OnChannelChange接口:channel拓扑关系发生变化响应函数
HasReader接口:有没有订阅者
GetReaders接口:获取订阅者列表

6.ServiceBase类和Service类

ServiceBase包含service_name_服务名称,虚函数接口destroy销毁服务,函数接口,service_name获取服务名字
Service类
node_name_属性:从构造函数传进来的,用于以后标识
service_callback_属性:服务的回调函数,也是服务的核心内容
request_callback_属性:目测无用属性
response_transmitter_属性:用于发送数据的指针,服务通讯是1to1的,发送者和接受者采用的是固定RTPS方式通讯
request_receiver_属性:用于接收数据的指针
request_channel_属性:这个没看懂用来做什么的,难道服务模式通讯还需要注册一个channel吗?
response_channel_属性:同上
service_handle_request_mutex_属性:服务处理锁,保证同一时刻只进行一个服务。
inited_属性:初始化标志
thread_属性:工作线程,循环读取服务的任务队列,为什么不是一个协程?我的猜测是协程可能由于优先级和任务队列得不到运行,采用线程机制,会立即执行,并且不会收到干扰。
queue_mutex_属性:任务队列锁
condition_属性:条件变量,来了任务好第一时间解锁干活
tasks_属性:任务队列
Init函数:对服务进行初始化,创建线程循环读取任务队列,有活立马干;创建接收器,接收器有个回调函数,就是收到数据立即创建一个std::function,加入到任务队列
destroy函数:销毁服务
Enqueue函数:任务加入到队列
Process函数:遍历队列,取出std::function去执行任务
HandleRequest函数:处理服务请求
SendResponse函数:发送服务处理请求之后的结果
为什么采用双锁(queue_mutex_和service_handle_request_mutex_)?如果采用一个锁,就会出现将任务存放至队列和执行任务互斥的情况,所以采用了双锁,取出数据的操作会手动解锁。

7.ClientBase类和Client类

ClientBase包含service_name_服务名称一个成员变量
虚函接口destroy:销毁服务
函数接口ServiceName:获取服务名字
纯虚接口ServiceIsReady:没做实现
保护接口WaitForServiceNanoseconds:每隔毫秒询问下服务状态,是否开启了,到指定时间任然未开启则返回未开启。
Client类
node_name_属性:从构造函数传进来的,用于以后标识
response_callback_属性:得到服务回应之后的动作,目前绑定到了本类HandleResponse接口
pending_requests_属性:unordered_map数据类型,保存所有发送的请求,使得可以多线程同时发送多个请求,即使上一个请求还没有得到回复,已经可以发送下一个了,数据回来的时候怎么知道此次回复针对的是哪个请求呢,每次请求都做一个标号,这个标号做key,具体内容做value形成了一个map
pending_requests_mutex_属性:专用于保护上述变量在多线程中的安全
request_transmitter_属性:用于发送数据的指针,服务通讯是1to1的,发送者和接受者采用的是固定RTPS方式通讯
response_receiver_属性:用于接收数据的指针
request_channel_属性:这个没看懂用来做什么的,难道服务模式通讯还需要注册一个channel吗?
response_channel_属性:同上
writer_id_属性:通过属性request_transmitter_的id接口函数获取的值,固定不变
sequence_number_属性:发送请求序列号
WaitForService接口:调用父类WaitForServiceNanoseconds,等待服务启动完成
Destroy接口:空实现
Init接口:创建request_transmitter_,绑定数据返回调用HandleResponse接口
SendRequest接口:调用AsyncSendRequest接口,会等待一会儿检查数据是否返回
AsyncSendRequest接口:调用request_transmitter_发送数据,并记录此次请求相关内容
ServiceIsReady接口:固定返回true
HandleResponse接口:收到服务器返回,会调用AsyncSendRequest的参数二的回调函数

8.仍存疑问

  • Node存储了所有的自己创建的所有reader,为什么没有存所有的writer?应该是reader有读取的开关Observe函数,而writer没有,有所有reader就可以随时开关想要读取的数据,Observe这个操作是干嘛的?是只调用一次就好了,还是要频繁的调用?
    后期解答:实际测试了一下,cyber的例子程序没有调用该函数,两个进程之间可以正常通讯
  • 上述四对类,都是父子一对一的关系,即独生子女关系。在我的理解里面独生子女父子类是完全没有意义的,类的继承就是为了多态和代码重用,独生子女类体现不了这种优势,完全可以用一个“绝户类“来实现所有功能。
    后期解答:但是这种关系比较微妙,微妙在父类是普通类,子类都是模板类,我们知道模板类不是一个类,而是一群类,可能就是为了这个原因吧,这种情况下只是有一个类能否达到想要的设计?还没搞太明白。
  • 只有reader没有write会生成对应的channel吗?
    后期解答:经过实测会的,cyber_monitor可以看得见。阅读源代码可以发现reader的初始化函数里面有调用JoinTheTopology函数,这个就是加入到拓扑图的操作。
  • Service和Client交互模式好像也离不开channel啊,虽然没有将这个channel注册到拓扑图里面,但是Client和Service都有两个Channel。这是个怎么回事?
    后期解答:我们知道cyber支持三种方式通讯,经过测试内部通讯INTRA是不需要有channel名字的,而Service和Client模式是固定采用的RTPS方式通讯,另外两种都需要设置channel名字,否则transmitter_和receiver_无法通讯。测试代码如下:
#include <iostream>
#include "cyber/examples/proto/examples.pb.h"
#include "cyber/cyber.h"
#include "cyber/time/rate.h"
#include "cyber/time/time.h"

using apollo::cyber::Rate;
using apollo::cyber::Time;
using apollo::cyber::examples::proto::Chatter;

int main(int argc, char* argv[]) {
  apollo::cyber::Init(argv[0]);
  auto node1 = apollo::cyber::CreateNode("node_name_");
  apollo::cyber::proto::RoleAttributes role;
  role.set_node_name("node_name_");
  //role.set_channel_name("/channel_name_");
  auto channel_id =
      apollo::cyber::common::GlobalData::RegisterChannel("/channel_name_");
  role.set_channel_id(channel_id);
  role.mutable_qos_profile()->CopyFrom(
      apollo::cyber::transport::QosProfileConf::QOS_PROFILE_SERVICES_DEFAULT);

  auto transport = apollo::cyber::transport::Transport::Instance();
  auto transmitter_ = transport->CreateTransmitter<Chatter>(
      role, apollo::cyber::proto::OptionalMode::RTPS);
  auto receiver_ = transport->CreateReceiver<Chatter>(
      role,
      [=](const std::shared_ptr<Chatter>& response,
          const apollo::cyber::transport::MessageInfo& message_info,
          const apollo::cyber::proto::RoleAttributes& reader_attr) {
        std::cout << response->seq() 
                  << " " << message_info.channel_id()
                  << " " << message_info.sender_id().ToString()
                  << " " << message_info.spare_id().ToString()
                  << std::endl;
      },
      apollo::cyber::proto::OptionalMode::RTPS);

  Rate rate(1.0);
  uint64_t seq = 0;
  while (apollo::cyber::OK()) {
    auto msg = std::make_shared<Chatter>();
    msg->set_timestamp(Time::Now().ToNanosecond());
    msg->set_lidar_timestamp(Time::Now().ToNanosecond());
    msg->set_seq(seq);
    transmitter_->Transmit(msg);
    seq++;
    rate.Sleep();
  }
  apollo::cyber::WaitForShutdown();
  return 0;
}
cyberrt的源码结构解析与模块设计中,可以看到其高度模块化的设计理念,这使得系统各部分的功能更加清晰,也便于维护扩展。以node模块为例,node类更像是一个代理类,其构造函数是私有的,这意味着外部不能直接创建node实例,而是通过定义的四种友元方式来实现。这种方式确保了node实例化的统一性安全性[^3]。 ### 源码结构解析 cyberrt的源码结构通常按照功能划分为多个模块,每个模块负责处理特定的任务。例如,`/cyber/node`模块`/cyber/service`模块分别处理节点管理信息服务请求。这种划分不仅有助于理解各个组件之间的关系,也为开发者提供了明确的接口用于开发新的功能或调试现有代码。 - **NodeChannelImpl类**:此模块实现了基于通道名字的发布订阅模式通信,支持进程内部、不同进程之间以及不同主机之间的数据传输。这类通信机制对于构建分布式系统至关重要,因为它允许组件间高效地交换信息而不必关心底层网络细节。 - **NodeServiceImpl类**:与此相对应的是服务模式下的通信支持,它处理基于服务的一问一答模式的数据交互。这两个主要成员共同构成了cyber框架下两种核心的消息传递机制。 ### 模块设计详解 除了上述提到的节点服务模块外,还有许多其他重要组成部分构成了整个cyberrt生态系统: - **CRoutine模块**:通过`MakeContext()`函数初始化协程上下文环境,这段代码展示了如何手动设置栈指针并填充参数到堆栈中的过程。这种方法虽然较为底层但能提供更细粒度控制权给程序员,以便于优化性能[^2]。 - **Cyber初始化流程**:当调用`apollo::Cyber::init()`时,会启动异步日志记录线程,该线程利用Google的日志库完成平台各业务模块的日志记录工作。这一设计保证了即使在高并发场景下也能保持良好的日志管理能力[^4]。 ### 实现细节探讨 从技术角度看,cyberrt的一些实现细节值得深入研究: - 数据结构方面采用protobuf(.proto文件)定义消息格式,这样做的好处是可以自动生成C++所需的数据结构,并且方便从文本文件导入导出数据[^1]。这对于快速原型设计及后期配置调整都非常有利。 ```cpp // 示例.proto文件片段 message ExampleMessage { int32 id = 1; string name = 2; } ``` - 在内存管理上,如CRoutine中的栈分配策略显示了对底层硬件特性的深刻理解运用,通过对栈指针直接操作来实现高效的上下文切换。 综上所述,cyberrt不仅在架构设计上有独到之处,在具体实现手法上也有很多亮点,这些都为其成为高性能自动驾驶软件平台奠定了坚实基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值