3、模块间通信实战:编写第一个Cyber节点、自定义消息类型、节点间收发数据、使用cyber_monitor调试
好,咱们今天来点真格的。前面聊了那么多Cyber的概念和架构,说白了都是纸上谈兵。这一章,我带你亲手写一个能跑起来的Cyber节点,再搞一个自定义消息类型,让两个节点互相传数据。最后,用cyber_monitor这个神器来抓包看看——嗯,这才是真正的模块联调入门。
3.1 编写你的第一个Cyber节点
先别急着写代码,咱们得搞清楚一件事:一个Cyber节点到底长什么样?
我个人习惯把Cyber节点想象成一个「独立的小机器人」。它有自己的名字,能说(发布消息)也能听(订阅消息)。在Apollo里,每个模块基本上就是一个或多个Cyber节点。
来,直接看代码。这是一个最简单的发布者节点:
#include "cyber/cyber.h"
#include "cyber/demo_base_proto/demo_base.pb.h"
using apollo::cyber::demo_base_proto::Demo;
int main(int argc, char* argv[]) {
// 1. 初始化Cyber框架
apollo::cyber::Init(argv[0]);
// 2. 创建节点,起个名字叫"talker"
auto talker_node = apollo::cyber::CreateNode("talker");
// 3. 创建发布者,指定消息类型和频道名
auto talker = talker_node->CreateWriter<Demo>("channel/chatter");
// 4. 循环发送消息
apollo::cyber::Rate rate(1.0); // 每秒发一次
uint64_t seq = 0;
while (apollo::cyber::OK()) {
auto msg = std::make_shared<Demo>();
msg->set_integer(seq++);
msg->set_text("Hello, this is my first Cyber node!");
talker->Write(msg);
AINFO << "Sent message: " << msg->DebugString();
rate.Sleep();
}
return 0;
}
这段代码我拆成四步讲:
- 初始化:
cyber::Init是必须的,它启动Cyber的后台线程。不调这行,后面全白搭。 - 创建节点:
CreateNode("talker"),名字别乱起,后面调试全靠它认人。 - 创建发布者:
CreateWriter<Demo>("channel/chatter"),模板参数是消息类型,字符串是频道名。两个节点要通信,频道名必须一致。 - 循环发送:
rate.Sleep()控制频率,Write()把消息发出去。
cyber::OK()这个判断很重要。当Cyber框架被关闭时(比如Ctrl+C),它会返回false,帮你优雅退出循环。别自己写死循环。
3.2 自定义消息类型
刚才用的Demo是Apollo自带的示例消息。但实际项目中,你肯定需要自己的数据结构。比如我要传一个「障碍物信息」,包含位置、速度、类型——那就得自己定义。
Cyber的消息类型基于Google的Protobuf。你只需要写一个.proto文件,然后编译一下就行。
创建一个my_msg.proto:
syntax = "proto2";
package apollo.cyber.my_msg;
message Obstacle {
optional int32 id = 1; // 障碍物ID
optional double x = 2; // 位置x
optional double y = 3; // 位置y
optional double speed = 4; // 速度
optional string type = 5; // 类型:CAR, PEDESTRIAN, BICYCLE
optional uint64 timestamp = 6; // 时间戳
}
然后修改BUILD文件:
proto_library(
name = "my_msg_proto",
srcs = ["my_msg.proto"],
)
cc_proto_library(
name = "my_msg_cc_proto",
deps = [":my_msg_proto"],
)
编译之后,你就可以在C++代码里#include "my_msg.pb.h",然后像用普通类一样使用Obstacle消息了。
3.3 节点间收发数据
有了发布者,还得有个订阅者。不然消息发出去没人收,那不成自言自语了吗?
订阅者节点的代码长这样:
#include "cyber/cyber.h"
#include "my_msg.pb.h"
using apollo::cyber::my_msg::Obstacle;
// 回调函数:收到消息时自动触发
void OnObstacle(const std::shared_ptr<Obstacle>& msg) {
AINFO << "Received obstacle: "
<< "ID=" << msg->id()
<< ", x=" << msg->x()
<< ", y=" << msg->y()
<< ", speed=" << msg->speed()
<< ", type=" << msg->type();
}
int main(int argc, char* argv[]) {
apollo::cyber::Init(argv[0]);
auto listener_node = apollo::cyber::CreateNode("listener");
// 订阅同一个频道
auto listener = listener_node->CreateReader<Obstacle>(
"channel/obstacle", OnObstacle);
apollo::cyber::WaitForShutdown();
return 0;
}
这里的关键点:
- 频道名必须一致:发布者用
"channel/obstacle",订阅者也用"channel/obstacle"。拼写错一个字符,数据就丢了。 - 回调函数:
OnObstacle会在每次收到消息时被调用。注意它是异步的,别在里面做耗时操作。 - WaitForShutdown:让主线程挂起,等待框架关闭信号。
你想想看,这两个节点可以跑在不同的进程里,甚至不同的机器上。Cyber的通信层帮你搞定了序列化、网络传输、线程调度——你只管写业务逻辑。
3.4 使用cyber_monitor调试
代码写完了,怎么知道数据有没有发出去?频道名对不对?消息内容正不正确?
这时候就该cyber_monitor出场了。它是Cyber自带的命令行调试工具,说白了就是一个「消息嗅探器」。
启动方式很简单:
# 在Apollo的Docker环境里
cyber_monitor
你会看到一个实时更新的频道列表:
ChannelName TypeName Hz
/channel/obstacle apollo.cyber.my_msg.Obstacle 1.0
/channel/chatter apollo.cyber.demo_base_proto.Demo 1.0
/tf apollo.transform.TransformStampeds 10.0
...
常用操作:
| 按键 | 功能 |
|---|---|
| ↑ ↓ | 上下移动选择频道 |
| → | 展开查看消息内容 |
| ← | 收起 |
| q | 退出 |
按右箭头展开一个频道,你会看到类似这样的内容:
channel/obstacle:
id: 1
x: 10.5
y: 20.3
speed: 5.2
type: "CAR"
timestamp: 1698123456
cyber_monitor来确认「障碍物消息到底有没有发出来」。有一次发现某个频道Hz一直为0,排查了半天,结果是发布者节点的频道名多写了一个斜杠。这种低级错误,用工具一眼就能看出来。
3.5 本章知识体系
为了让你对整个流程有个全局印象,我画了一张图:
这张图把整个流程串起来了。你写代码的时候,脑子里要有这个图:左边是发布者,中间是频道,右边是订阅者。调试工具在下面兜底,随时帮你检查数据流。
好了,这一章的内容就到这里。代码示例、自定义消息、收发调试,该有的都有了。剩下的就是你自己动手敲一遍——光看是学不会的,我当年也是踩了无数坑才摸熟的。
可可精品资料库