4. 通信机制实战(上):话题通信——发布者与订阅者实现、自定义消息接口、QoS配置策略

好,我们进入通信机制实战环节。说实话,话题通信是 ROS2 里最核心、最常用的通信方式。你想想看,机器人身上那么多传感器、执行器,它们之间怎么高效地传递数据?靠的就是话题。这一章,我会带你亲手实现一个发布者和订阅者,然后聊聊自定义消息和 QoS 配置——这些都是我实际项目中反复踩坑后总结出来的经验。

4.1 发布者与订阅者的基本实现

先从一个最简单的例子开始。假设我们要发布一个字符串消息,订阅者收到后打印出来。嗯,这就像广播电台和收音机的关系——发布者只管发,订阅者只管收,双方不需要知道对方是谁。

4.1.1 编写发布者节点

我个人习惯用 C++ 写 ROS2 节点,因为性能更好。不过 Python 也完全没问题,看你的项目需求。下面是一个标准的发布者代码:

// publisher.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class SimplePublisher : public rclcpp::Node
{
public:
    SimplePublisher() : Node("simple_publisher")
    {
        publisher_ = this->create_publisher<std_msgs::msg::String>("topic", 10);
        timer_ = this->create_wall_timer(
            std::chrono::seconds(1),
            std::bind(&SimplePublisher::timer_callback, this));
    }

private:
    void timer_callback()
    {
        auto message = std_msgs::msg::String();
        message.data = "Hello, ROS2!";
        RCLCPP_INFO(this->get_logger(), "Publishing: '%s'", message.data.c_str());
        publisher_->publish(message);
    }

    rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
    rclcpp::TimerBase::SharedPtr timer_;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<SimplePublisher>());
    rclcpp::shutdown();
    return 0;
}
小提示: 创建发布者时第二个参数是队列大小。我建议设成 10,既能保证消息不丢,又不会占用太多内存。如果你在项目中遇到消息丢失,可以适当调大这个值。

4.1.2 编写订阅者节点

订阅者这边更简单。它只需要注册一个回调函数,当有消息到达时自动触发:

// subscriber.cpp
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"

class SimpleSubscriber : public rclcpp::Node
{
public:
    SimpleSubscriber() : Node("simple_subscriber")
    {
        subscription_ = this->create_subscription<std_msgs::msg::String>(
            "topic", 10, std::bind(&SimpleSubscriber::topic_callback, this, std::placeholders::_1));
    }

private:
    void topic_callback(const std_msgs::msg::String::SharedPtr msg)
    {
        RCLCPP_INFO(this->get_logger(), "I heard: '%s'", msg->data.c_str());
    }

    rclcpp::Subscription<std_msgs::msg::String>::SharedPtr subscription_;
};

int main(int argc, char *argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<SimpleSubscriber>());
    rclcpp::shutdown();
    return 0;
}
注意: 订阅者的回调函数不要做耗时操作。我曾经在回调里写了个图像处理算法,结果导致消息队列溢出,系统直接崩溃。如果确实需要处理复杂逻辑,请用单独的线程或异步处理。

4.2 自定义消息接口

标准消息类型(比如 String、Int32)只能满足简单需求。实际项目中,你往往需要传递更复杂的数据结构。比如,我要发布一个包含位置和姿态的机器人状态消息,那就得自己定义接口。

4.2.1 创建自定义消息文件

在 ROS2 中,自定义消息放在 .msg 文件中。假设我们创建一个 RobotStatus.msg:

# RobotStatus.msg
int32 robot_id
string robot_name
float64 position_x
float64 position_y
float64 orientation_z
bool is_moving

这个文件放在你的功能包里的 msg 目录下。然后需要在 CMakeLists.txt 中添加:

rosidl_generate_interfaces(${PROJECT_NAME}
  "msg/RobotStatus.msg"
)

以及在 package.xml 中添加依赖:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
关键点: 自定义消息的字段类型可以是基本类型(int32、float64 等),也可以是其他消息类型。比如你可以嵌套使用 geometry_msgs/Point 来表示位置。

4.2.2 在代码中使用自定义消息

编译成功后,你就可以像使用标准消息一样使用自定义消息了:

#include "your_package/msg/robot_status.hpp"

// 发布者
auto publisher = this->create_publisher<your_package::msg::RobotStatus>("robot_status", 10);

// 创建消息
auto msg = your_package::msg::RobotStatus();
msg.robot_id = 1;
msg.robot_name = "TurtleBot";
msg.position_x = 1.5;

嗯,这里要注意:消息字段的命名风格是蛇形命名法(snake_case),ROS2 会自动帮你转换成驼峰命名法(camelCase)的 C++ 代码。

4.3 QoS 配置策略

QoS(Quality of Service)是 ROS2 相比 ROS1 最大的改进之一。说白了,它让你能精细控制消息的可靠性、持久性等属性。我刚开始接触时也觉得复杂,但用熟了就会发现它真的很强大。

4.3.1 QoS 的核心参数

参数 说明 常用值
Reliability 可靠性策略 RELIABLE(保证送达)/ BEST_EFFORT(尽力而为)
Durability 持久性策略 VOLATILE(不保留)/ TRANSIENT_LOCAL(保留最后一条)
Depth 队列深度 通常设为 10
Liveliness 活性检测 AUTOMATIC / MANUAL_BY_TOPIC

4.3.2 实际配置示例

假设你有一个激光雷达,数据量很大但允许丢帧。这时候用 BEST_EFFORT 策略最合适:

auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
qos.reliability(RMW_QOS_POLICY_RELIABILITY_BEST_EFFORT);
qos.durability(RMW_QOS_POLICY_DURABILITY_VOLATILE);

auto subscription = this->create_subscription<sensor_msgs::msg::LaserScan>(
    "scan", qos, callback);

反过来,如果是控制指令,必须保证送达,那就用 RELIABLE:

auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
qos.reliability(RMW_QOS_POLICY_RELIABILITY_RELIABLE);
qos.durability(RMW_QOS_POLICY_DURABILITY_TRANSIENT_LOCAL);
避坑指南: 我曾经在项目中把发布者和订阅者的 QoS 配置设得不一致,结果消息死活收不到。记住:发布者和订阅者的 QoS 必须兼容,否则通信会失败。ROS2 会尝试自动协商,但最好手动保持一致。

4.4 本章知识体系

下面这张图展示了话题通信的核心流程和本章涉及的关键概念:

发布者节点 Publisher 订阅者节点 Subscriber 话题 (Topic) 消息通道 QoS 配置策略 Reliability (RELIABLE / BEST_EFFORT) | Durability (VOLATILE / TRANSIENT_LOCAL) | Depth 自定义消息接口 .msg 文件定义 → rosidl_generate_interfaces → 生成 C++/Python 代码

4.5 实战经验总结

最后,分享几个我在项目中积累的经验:

  • 消息类型选择: 能用标准消息就别自己定义。比如位置信息用 geometry_msgs/Pose,图像用 sensor_msgs/Image,这样能保证和其他包的兼容性。
  • QoS 调试技巧: 如果发现消息收不到,先用 ros2 topic echo /topic_name 看看话题上有没有数据。再用 ros2 topic info /topic_name 查看发布者和订阅者的 QoS 配置。
  • 性能优化: 高频话题(比如 IMU 数据)建议用 BEST_EFFORT 和较小的队列深度,避免内存暴涨。低频控制指令用 RELIABLE 保证可靠性。
  • 命名规范: 自定义消息的包名、消息名、字段名都要有意义。我见过有人用 msg1.msg 这种名字,三个月后自己都看不懂了。
我的习惯: 每次创建新消息类型时,我都会先在 .msg 文件里写清楚每个字段的注释。这样不仅方便自己,也方便团队协作。你想想看,半年后回头维护代码,注释就是最好的文档。