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