第二章:Cyber RT通信机制解析与实践
第一节:Cyber RT 通信机制简介
1.1 话题通信
模式:
以发布订阅的方式实现不同节点之间数据交互的通信模式。
如图1-1所示,Listener-Talker通信首先创建了两个Node,分别是Talker Node和 Listener Node。
每个Node实例化Writer类和Reader类对Channel进行消息的读写。
Writer和Reader通过Topic连接,对同一块共享内存(Channel)进行读写处理。
Talker Node 为了实现其“诉说”的功能,实例化Writer,通过Writer来对Channel进行消息写操作。
Listener Node为了实现其“聆听”功能,实例化reader类,通过Reader来对channel进行读操作。
场景:
话题通信方式适合于持续性通信的应用场景,比如雷达信号,摄像头图像信息这类数据的传输。
使用:
Listener-Talker通信一方主动送消息,一方被动接收。
我们想要一直获取车的速度,该需求不需要向发送方返回什么消息,也不需要发送方对消息进行进一步处理。所以我们选择了Listener-Talker通信方式实现该功能。
数据定义:
话题通信中用的的数据格式的定义car message 定义在car.proto中。
1.2 服务通信
模式:
以请求响应的方式实现不同节点之间数据交互的通信模式。
如图1-2所示,Server-Client通信可以在客户端发出消息请求时,服务端才进⾏请求回应,并将客户端所需的数据返回给客户端。
场景:
我们想要获得⼩⻋的详细信息,⽐如⻋牌这些,但是⼜不需要⼀直获得该信息,想要在需要知道这些信息的时候请求⼀下就好,于是考虑⽤Server- Client通信实现该功能。
使用:
该通信模式适合临时的消息传输,适⽤于不需要持续性发送数据的场景。
数据定义:
其传输的数据定义依然在对应的proto⽂件中。
1.3 参数通信
模式:
以共享的方式实现不同节点之间数据交互的通信模式。
参数服务器是基于服务实现的,包含客户端和服务器端,服务端节点可以存储数据,客户端节点可以访问服务端节点操作数据,这个过程虽然基于请求响应的,但是无需自己实现请求与响应,此过程已经被封装,调用者只需要通过比较简单友好的API就可以实现参数操作。
场景:
自动驾驶场景中有一些参数比如该车的最高限速、最多乘客以及是否自动驾驶等需要被各个模块使用数据,比如是否自动驾驶这个参数可能同时影响这很多模块,也可能被很多模块运行时所更改。
这些数据如何实现在不同模块之间的共享呢?
使用:
类似于“全局变量”的方式来存储这些参数,并定义一些自定义参数来进行使用。
数据定义:
Cyber中设计了全局参数服务器来实现这个功能,其通信基于RTPS协议。该通信方式服务端和客户端都可以设置参数和更改参数。
第二节:数据通信基础Protobuf
1.3 Protobuf简介
2.1 Protobuf简介
Protobuf 是 Google 公司开发的一种跨语言和平台的序列化数据结构的方式,是一个灵活的、高效的用于序列化数据的协议,与 XML 和 JSON 格式相比,Protobuf 更小、更快、更便捷。
Protobuf 是跨语言的,并且自带一个编译器( protoc ),只需要用protoc进行编译,就可以编译成 Java、Python、C++、C#、Go 等多种语言代码,然后可以直接使用,不需要再写其它代码,自带有解析的代码。只需要将要被序列化的结构化数据定义一次(在 .proto 文件定义),便可以使用特别生成的源代码(使用protobuf提供的生成工具)轻松的使用不同的数据流完成对结构数据的读写操作。甚至可以更新 .proto 文件中对数据结构的定义而不会破坏依赖旧格式编译出来的程序。其优点如下:
- 性能效率高:序列化后字节占用空间比 XML 少3-10倍,序列化的时间效率比 XML 快20-100倍。
- 使用便捷便捷:将对结构化数据的操作封装成一个类,便于使用。
- 兼容性高:通信两方使用同一数据协议,当有一方修改了数据结构,不会影响另一方的使用。
- 跨语言:支持 Java,C++,Python、Go、Ruby 等多种语言。
2.2 Protobuf 文件编写
为了方便讲解,使用cyber/examples/proto/examples.proto文件来讲解Protobuf的结构:
Protobuf有几个部分构成:
- syntax :表示使用Protobuf的版本,目前Protobuf支持proto3,但在Apollo中使用的是proto2;
- package: 表示该文件的路径;
- message:表示一种数据结构,message后面跟的是数据结构名字,括号里的字段定义格式为:字段规则 数据类型 字段名称 字段编号。
字段规则主要有三种
- required:调用时必须提供该字段的值,否则该消息被视为“未初始化”,官方不建议使用,当把字段规则改为其他规则会存在兼容性问题。
- optional:该字段的值可以设置也可以不设置,会根据数据类型生成一个默认的值。
- repeated:类似于动态数组,可以存储多个同类型的数据。
# examples.protosyntax = "proto2";package apollo.cyber.examples.proto;message SamplesTest1 {optional string class_name = 1;optional string case_name = 2;};message Chatter {optional uint64 timestamp = 1;optional uint64 lidar_timestamp = 2;optional uint64 seq = 3;optional bytes content = 4;};message Driver {optional string content = 1;optional uint64 msg_id = 2;optional uint64 timestamp = 3;};
2.3 Protobuf编译
Protobuf的编译要分为两个步骤:
- 首先要根据.proto文件生成proto库;
- 然后再根据生产的proto库生成C++相关的源文件。
这个源文件是C++语言自动编写的,可以被C++程序自动识别。每一个message会被解析生一个类,里面的字段就相当于这个类的属性。在源文件中也会根据属性生成额外的成员,如获取和设置属性的函数。
package(default_visibility = ["//visibility:public"])#1、生成proto库proto_library(name = "examples_proto",srcs = ["examples.proto"],)#2、生成源文件cc_proto_library(name = "examples_cc_proto",deps = [":examples_proto",],)
代码解析:
- 我们使用Bazel构建系统的BUILD文件,用于生成proto库和相关的源文件。
- 首先,通过proto_library规则定义了一个名为"examples_proto"的proto库,它使用"examples.proto"作为源文件。
- 然后,通过cc_proto_library规则定义了一个名为"examples_cc_proto"的源文件生成规则。它依赖于"examples_proto"库,并使用该库生成相关的C++源文件。
- 这些规则中的名称是任意定义的,您可以根据需要进行更改。
2.4 小案例
目的:使用Protobuf来定义数据格式,在main程序中设置数据值并输出。
流程:
<1> 创建本节实验工程目录
<2> 编写Apollo包管理相关的BUILD和cyberfile.xml文件文件
<3> 编写proto文件及BUILD文件;
<4> 编写主代码及BUILD文件:
<5> 编译代码目录
<6> 运行可执行文件
<1> 创建本节实验工程目录:
cyber_demo|-- cyber_03|-- proto|-- BUILD|-- car_msg.proto|-- test_proto|-- BUILD|-- car.cc|--BUILD|--cyberfile.xml|--cyber_demo.BUILD
<2> 编写Apollo包管理相关的BUILD和cyberfile.xml文件
- BUILD文件内容:
load("//tools/install:install.bzl", "install", "install_src_files")install(name = "install",data = ["cyber_demo.BUILD","cyberfile.xml",],deps = ["//cyber_demo/cyber_03/test_proto:install",],)install_src_files(name = "install_src",src_dir = ["."],dest = "cyber_demo/src",filter = "*",deps = ["//cyber_demo/cyber_03/test_proto:install_src",])
编写cyberfile文件:
<package><name>cyber_demo</name><version>1.0.0</version><description>cyber_demo</description><maintainer email="AD-platform">AD-platform@baidu.com</maintainer><type>module</type><src_path>//cyber_demo</src_path><license>BSD</license><author>Apollo</author><depend type="binary" src_path="//cyber" repo_name="cyber">cyber-dev</depend><depend lib_names="protobuf" repo_name="com_google_protobuf">3rd-protobuf-dev</depend><builder>bazel</builder></package>
<3> 编写proto源文件及BUILD文件;
编写proto文件,文件中定义了车辆的信息:
syntax = "proto2";package apollo.cyber.test.proto;message CarMsg {required string owner = 1;optional string license_plate = 2;optional uint64 max_passenger = 3;repeated string car_info = 4;}
编写proto的BUILD文件:
load("@rules_proto//proto:defs.bzl", "proto_library")load("@rules_cc//cc:defs.bzl", "cc_proto_library")load("//tools:python_rules.bzl", "py_proto_library")package(default_visibility = ["//visibility:public"])proto_library(name = "car_msg_proto",srcs = ["car_msg.proto"],)cc_proto_library(name = "car_msg_cc_proto",deps = [":car_msg_proto"],)
<4> 编写主代码及BUILD文件
通过car.cc输出车辆基本信息:
#include "test/proto/car_msg.pb.h"using namespace std;int main(){apollo::cyber::test::proto::CarMsg car;car.set_owner("apollo");car.set_license_plate("京A88888");car.set_max_passenger(6);car.add_car_info("SUV"); //车型car.add_car_info("Red"); //车身颜色car.add_car_info("electric"); //电动string owner = car.owner();string license_plate = car.license_plate();uint64_t max_passenger = car.max_passenger();cout << "owner:" << owner << endl;cout << "license_plate:" << license_plate << endl;cout << "max_passenger:" << max_passenger << endl;for (int i = 0; i < car.car_info_size(); ++i){string info = car.car_info(i);cout << info << " ";}cout << endl;return 0;}
注意
本段代码中内容需要进行
#include "test/proto/car_msg.pb.h"--------------------需要替换为---------------------------#include "cyber_demo/cyber_03/proto/car_msg.pb.h"
编辑car.cc的BUILD文件:
load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_library")load("//tools/install:install.bzl", "install", "install_src_files")load("//tools:cpplint.bzl", "cpplint")package(default_visibility = ["//visibility:public"])cc_binary(name = "car",srcs = ["car.cc"],deps = ["//test/proto:car_msg_cc_proto"],)install(name = "install",runtime_dest = "test/bin",targets = [":car"],)install_src_files(name = "install_src",src_dir = ["."],dest = "test/src/cyberatest",filter = "*",)
注意
本段BUILD代码中内容需要进行修改
cc_binary(name = "car",srcs = ["car.cc"],deps = ["//test/proto:car_msg_cc_proto"],)--------------------需要替换为---------------------------cc_binary(name = "car",srcs = ["car.cc"],deps = ["//cyber_demo/cyber_03/proto:car_msg_cc_proto"],)
<5> 编译代码
cd /apollo_workspacebuildtool build -p cyber_demo/
编译结果如图所示:
编译完成后会在 /opt/apollo/neo/bin/的目录下生成可执行文件car,如下图所示:
<6> 运行可执行文件
cd /opt/apollo/neo/bin/./car
运行结果如下图:
所有者:apollo
车牌号:京A88888
最大乘客人数:6
SUV 红色 电动
内容导入:
第三节:Cyber RT通信机制解析与实践
1. 实践简介
在第一章中,鹏飞老师已经说明了Node、Channel等Cyber的基础概念,本节我们基于这些基础概念,重点介绍 cyber 中的三种通信方式之一 - Listener 和 Talker通讯原理。为了更好的说明 Listener 和 Talker通信机制的用法,本节我们通过实现一个案例来说明该部分内容。
案例如下,假设我们拥有了一辆自己的“无人车“,apollo号,现在我们想通过通信,获得该车辆的“实时”速度、车的详细信息以及车的通用参数。那么现在会有几个问题:
- 车本身有的变量有哪些,应该在哪里定义?
- 实现“实时”车速的获取和实现车的详细信息等应该用什么通信方式?在哪里实现?
- 实现的代码应该如何运行起来?
不要着急,接来下我们就开始用Cyber实现这个小demo,并一一解答以上疑问。该案例我们创建在/apollo/workspace/test文件下,命名为communication整体目录结构如下:
2. 编写C++话题通信实践案例
1. proto文件编写
通过第一节内容,我们知道了proto文件的使用方法,那么这一章,我们来自己编写一个proto文件,来实现我们“车”的变量定义,在后续的三种通信方式的案例中都是用这一数据定义。
对car.proto文件进行编写,其内容如下:
2.1 实现小车的实时速度获取——Listener-Talker通信
2. talker.cc 文件编写
- 我们来编写一个talker.cc来实现主动对话。
3. listener.cc 文件编写
- 编写一个listener来实现对talker发送过来的内容进行接收。
3. 运行与测试
1. bazel 编译文件编写
编写bazel编译文件(编写在cyber/examples/cyber_test/BUILD中)。
2. 编译:
使用apollo 包管理开发方式提供的buildtool工具
如下图所示,则编译成功。
3. 运行案例
首先,先将输出方法改为控制台输出。
export GLOG_alsologtostderr=1
打开两个终端,都进入Apollo的docker环境,一个终端运行talker,另一个运行listener,会发现listener运行后开始接收talker发送的小车速度的消息。
结果显示:
结果显示:
总体运行效果如下图所示。
第四节:Cyber RT 本地部署之话题通信实践案例
1. Apollo 本地安装部署
2. 创建和进入 Apollo 环境容器
1. 创建工作空间
创建并进入目录
mkdir application-democd application-demo
2. 启动 apollo 环境容器
aem start
如果一切正常,将会见到类似下图的提示:
3. 进入 apollo 环境容器
aem enter
脚本执行成功后,将显示以下信息,您将进入 Apollo 的运行容器:
工作空间文件夹将被挂载到容器的 /apollo_workspace 中。
4. 初始化工作空间
aem init
至此 Apollo 环境管理工具及容器已经安装完成。
3. Protobuf、talker-listener实践案例演示
1. Protobuf 案例:
打印日志
export GLOG_alsologtostderr=1
运行car:
cd /apollo_workspace/bazel-bin/test_proto/./car
car 运行结果如图所示
2. talker-listener 案例:
talker进入环境配置打印日志:
aem startaem enterexport GLOG_alsologtostderr=1
运行talker:
cd /apollo_workspace/bazel-bin/test/communication/./talker
talker 运行结果如图所示
listener进入环境配置打印日志:
aem startaem enterexport GLOG_alsologtostderr=1
运行listener:
cd /apollo_workspace/bazel-bin/test/communication/./listener
listener 运行结果如图所示
课后学习内容:
作业:
编写Python话题通信实现。
练习一: 实现小车详细信息的获取——Server-Client通信
1.1 Server-Client 简介
我们想要获得小车的详细信息,比如车牌这些,但是又不需要一直获得该信息,想要在需要知道这些信息的时候请求一下就好,于是考虑用Server- Client通信实现该功能。如图2-1所示,Server-Client通信可以在客户端发出消息请求时,服务端才进行请求回应,并将客户端所需的数据返回给客户端。该通信模式适合临时的消息传输,适用于不需要持续性发送数据的场景。其传输的数据定义依然在对应的proto文件中。
1.2 Server-Client 案例实现
编写一个Server来对Client请求的消息进行回应,这里我们返回车的车牌号、车主、已行驶公里数等信息。
编写一个Client来请求车的详细信息,并获取数据。
编写bazel编译文件(编写在cyber/examples/cyber_test/BUILD中)。
- 编译:
仍然打开两个终端,进入apollo的docker环境后,进行编译,其BUILD文件和talker.cc等为同一个文件,所以编译语句相同,也可以精准编译,先编译server再编译client, 这里为了方便整体编译了。
- 运行:
运行成功后结果如下:
练习二: 实现小车通用参数的设置——Parameter Server-Client
2.1 Parameter Server-Client 简介
有一些参数比如该车的最高限速,最多乘客以及是否自动驾驶等需要被各个模块所使用,比如是否自动驾驶这个参数可能同时影响这很多模块,也可能被很多模块运行时所更改。我们希望有一个类似于“全局变量”的方式来存储这些参数,并定义一些自定义参数来进行使用。Cyber中设计了全局参数服务器来实现这个功能,其通信依然基于RTPS协议,如图2-8所示。该通信方式服务端和客户端都可以设置参数和更改参数。
2.2 Parameter Server-Client 案例实现
编写param_client.cc。
编写编译文件(编写在cyber/examples/cyber_test/BUILD中)。
- 编译:
还是打开两个终端,进入apollo的docker环境后,先进行编译。
buildtool build test/communication/
- 运行:
好啦,至此通过“小车”案例已经介绍完了Cyber三种通信方式的简单用法,快动起手来试试吧~