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;
}

这段代码我拆成四步讲:

  1. 初始化:cyber::Init是必须的,它启动Cyber的后台线程。不调这行,后面全白搭。
  2. 创建节点:CreateNode("talker"),名字别乱起,后面调试全靠它认人。
  3. 创建发布者:CreateWriter<Demo>("channel/chatter"),模板参数是消息类型,字符串是频道名。两个节点要通信,频道名必须一致。
  4. 循环发送: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消息了。

注意:我曾经犯过一个低级错误——字段编号重复了。Protobuf里每个字段的编号(=1, =2, =3...)必须唯一,而且一旦上线就别改了。改了会导致新旧版本解析错乱,数据全乱套。

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 本章知识体系

为了让你对整个流程有个全局印象,我画了一张图:

Cyber节点通信流程 发布者节点 CreateNode("talker") CreateWriter<Obstacle> writer->Write(msg) Cyber Channel "channel/obstacle" 消息队列 Buffer 序列化/反序列化 订阅者节点 CreateNode("listener") CreateReader<Obstacle> 回调函数处理 调试工具:cyber_monitor → 实时查看频道列表、消息内容、Hz频率 关键点:频道名一致、消息类型匹配、回调函数轻量

这张图把整个流程串起来了。你写代码的时候,脑子里要有这个图:左边是发布者,中间是频道,右边是订阅者。调试工具在下面兜底,随时帮你检查数据流。

我的建议:刚开始学的时候,别急着写复杂逻辑。先跑通一个「Hello World」级别的收发,确认环境没问题。然后再加自定义消息类型,最后再上业务逻辑。一步一个脚印,稳得很。

好了,这一章的内容就到这里。代码示例、自定义消息、收发调试,该有的都有了。剩下的就是你自己动手敲一遍——光看是学不会的,我当年也是踩了无数坑才摸熟的。

可可精品资料库