ROS 2 怎么创建发布者和订阅者?把 QoS 一起讲明白的零基础笔记
简介
刚开始学 ROS 2 的时候,很多人会先学发布者和订阅者,但很快又会遇到一个新词:QoS。
于是问题就来了:
- 发布者和订阅者到底怎么创建?
create_publisher(..., 10)里的10是什么?- 为什么有时候 topic 名和消息类型都对了,还是收不到消息?
reliable()、best_effort()、transient_local()又是什么意思?
其实这几件事可以放在一起理解。
你可以先记住一句话:
发布者和订阅者负责“发消息”和“收消息”,QoS 负责规定“消息怎么传”。
这篇笔记会用尽量通俗的方式,带你把这几件事一次搞懂。
一、什么是发布者和订阅者?
在 ROS 2 里,不同节点之间经常需要交换数据。
比如:
- 摄像头节点发布图像
- 雷达节点发布扫描数据
- 控制节点发布速度指令
- 状态节点发布当前状态
这里就有两个角色。
1. 发布者(Publisher)
发布者就是“发消息的人”。
它负责把消息发到某个 topic 上。
2. 订阅者(Subscriber)
订阅者就是“收消息的人”。
它负责监听某个 topic,只要这个 topic 上有新消息,它就会收到。
3. Topic
topic 可以理解成一个“频道”。
发布者把消息发到这个频道。
订阅者订阅这个频道,然后接收消息。
二、什么是 QoS?
QoS 全称是 Quality of Service,中文通常叫“服务质量策略”。
你可以把它理解成:
消息在传输之前,要先约定好一套规则。
比如:
- 消息能不能丢?
- 要不要尽量保证送达?
- 要缓存多少条?
- 后来才加入的订阅者,能不能收到之前发过的消息?
这些规则合在一起,就是 QoS。
所以你可以这样理解:
- 发布者 / 订阅者:解决“谁发谁收”
- QoS:解决“消息按什么规则发和收”
三、创建发布者和订阅者最核心的两个函数
在 ROS 2 C++ 里,发布者和订阅者通常都在节点内部创建。
最核心的两个函数就是:
创建发布者
this->create_publisher<消息类型>("topic名字", qos);
创建订阅者
this->create_subscription<消息类型>(
"topic名字",
qos,
回调函数
);
这里的 qos 可以直接写成一个数字,也可以写成 rclcpp::QoS(...)。
四、为什么有人写 10,有人写 rclcpp::QoS(10)?
你可能见过这两种写法:
this->create_publisher<std_msgs::msg::String>("topic", 10);
this->create_publisher<std_msgs::msg::String>(
"topic",
rclcpp::QoS(10)
);
对于初学者来说,可以先这样理解:
- 写
10:相当于给了一个最基本的队列深度 - 写
rclcpp::QoS(10):表示“我要显式配置 QoS”
而且以后你想继续加规则,比如:
reliable()best_effort()transient_local()
就必须从 rclcpp::QoS(...) 这种写法往下接。
例如:
auto qos = rclcpp::QoS(10).reliable();
这比只写一个 10 更清楚。
五、先看一个最简单的发布者(带 QoS)
下面是一个最基础的 C++ 发布者。
它每隔 500ms 发布一条字符串消息,并且使用了 QoS。
#include <chrono>
#include <memory>
#include <string>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
using namespace std::chrono_literals;
class MinimalPublisher : public rclcpp::Node
{
public:
MinimalPublisher() : Node("minimal_publisher"), count_(0)
{
auto qos = rclcpp::QoS(10).reliable();
publisher_ = this->create_publisher<std_msgs::msg::String>(
"topic",
qos
);
timer_ = this->create_wall_timer(
500ms,
std::bind(&MinimalPublisher::timer_callback, this)
);
}
private:
void timer_callback()
{
auto msg = std_msgs::msg::String();
msg.data = "Hello, ROS 2! " + std::to_string(count_++);
RCLCPP_INFO(this->get_logger(), "发布消息: '%s'", msg.data.c_str());
publisher_->publish(msg);
}
rclcpp::Publisher<std_msgs::msg::String>::SharedPtr publisher_;
rclcpp::TimerBase::SharedPtr timer_;
size_t count_;
};
int main(int argc, char * argv[])
{
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<MinimalPublisher>());
rclcpp::shutdown();
return 0;
}
六、这段发布者代码该怎么理解?
1. Node("minimal_publisher")
这是节点名字。
也就是说,这个程序启动后,在 ROS 2 里它叫 minimal_publisher。
2. auto qos = rclcpp::QoS(10).reliable();
这句就是在配置 QoS。
你可以先把它理解成:
- 保留最近 10 条消息
- 并且尽量保证消息可靠送达
其中:
10表示队列深度reliable()表示可靠传输
3. create_publisher
publisher_ = this->create_publisher<std_msgs::msg::String>(
"topic",
qos
);
这句话的意思是:
- 创建一个发布者
- 消息类型是
std_msgs::msg::String - topic 名字叫
"topic" - 它使用上面设置好的
qos
4. publisher_->publish(msg)
这句表示:
把消息真正发出去。
七、再看一个最简单的订阅者(带 QoS)
#include <memory>
#include "rclcpp/rclcpp.hpp"
#include "std_msgs/msg/string.hpp"
class MinimalSubscriber : public rclcpp::Node
{
public:
MinimalSubscriber() : Node("minimal_subscriber")
{
auto qos = rclcpp::QoS(10).reliable();
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic",
qos,
std::bind(&MinimalSubscriber::topic_callback, this, std::placeholders::_1)
);
}
private:
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
{
RCLCPP_INFO(this->get_logger(), "收到消息: '%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<MinimalSubscriber>());
rclcpp::shutdown();
return 0;
}
八、这段订阅者代码在干什么?
1. 创建节点
class MinimalSubscriber : public rclcpp::Node
说明我们写了一个订阅者节点。
2. 配置 QoS
auto qos = rclcpp::QoS(10).reliable();
这里和发布者一样,也用了相同的 QoS。
这很重要。
因为发布者和订阅者要想顺利通信,QoS 最好是兼容的。
3. 创建订阅者
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic",
qos,
std::bind(&MinimalSubscriber::topic_callback, this, std::placeholders::_1)
);
它的意思是:
- 订阅
"topic" - 消息类型是
std_msgs::msg::String - 使用
qos - 只要收到消息,就调用
topic_callback
4. 回调函数
void topic_callback(const std_msgs::msg::String::SharedPtr msg) const
这个函数可以理解成:
一旦收到消息,就自动执行这里面的代码。
在这个例子里,就是把收到的消息打印出来。
九、QoS 里最常见的几个配置
零基础先记住下面 3 个就够用了。
1. reliable()
auto qos = rclcpp::QoS(10).reliable();
意思是:
尽量保证消息送达。
适合:
- 控制命令
- 重要状态消息
- 不希望轻易丢失的数据
2. best_effort()
auto qos = rclcpp::QoS(5).best_effort();
意思是:
能送就送,丢一点也没关系。
适合:
- 激光雷达
- 相机
- IMU
- 高频传感器数据
因为这类数据往往更关心“最新的一帧”,而不是“旧数据必须补回来”。
3. transient_local()
auto qos = rclcpp::QoS(1).transient_local().reliable();
意思是:
发布者会保留历史消息,后来才启动的订阅者也有机会收到之前发过的消息。
适合:
- 地图
- 静态配置
- 初始化信息
- 一次发布但希望后来者也能看到的数据
十、初学者怎么选 QoS?
如果你刚入门,可以直接照下面记。
场景 1:普通消息
比如:
- 状态信息
- 普通控制信息
推荐:
auto qos = rclcpp::QoS(10).reliable();
场景 2:高频传感器
比如:
- 雷达
- 相机
- IMU
推荐:
auto qos = rclcpp::QoS(5).best_effort();
场景 3:静态消息 / 地图 / 初始化数据
推荐:
auto qos = rclcpp::QoS(1).transient_local().reliable();
十一、为什么有时候收不到消息?很可能就是 QoS 问题
很多初学者都会遇到这种情况:
- topic 名一样
- 消息类型一样
- 发布者也启动了
- 订阅者也启动了
- 但就是收不到消息
这时候除了检查 topic 和类型,还要检查 QoS。
因为在 ROS 2 里:
发布者和订阅者只有在 QoS 兼容时,才会建立连接。
你可以先简单记住一句话:
如果两边“通信规则”差太多,就连不上。
十二、一个很常见的 QoS 坑
发布者:
auto qos = rclcpp::QoS(5).best_effort();
订阅者:
auto qos = rclcpp::QoS(10).reliable();
这时候就可能有问题。
因为:
- 发布者说:我只是“尽力发”
- 订阅者却要求:你必须“可靠发”
这就容易不兼容。
所以初学者最稳妥的做法就是:
发布者和订阅者先用同样的 QoS。
十三、最小记忆模板
发布者模板
auto qos = rclcpp::QoS(10).reliable();
publisher_ = this->create_publisher<std_msgs::msg::String>(
"topic",
qos
);
订阅者模板
auto qos = rclcpp::QoS(10).reliable();
subscription_ = this->create_subscription<std_msgs::msg::String>(
"topic",
qos,
std::bind(&MyNode::callback, this, std::placeholders::_1)
);
发布消息
publisher_->publish(msg);
让节点持续运行
rclcpp::spin(node);
十四、怎么创建包、编译和运行?
1. 创建 C++ 包
ros2 pkg create --build-type ament_cmake --license Apache-2.0 cpp_pubsub
2. 编译
cd ~/ros2_ws
colcon build
3. source 环境
source install/setup.bash
4. 运行发布者
ros2 run cpp_pubsub talker
5. 运行订阅者
ros2 run cpp_pubsub listener
十五、初学者最容易踩的坑
坑 1:topic 名不一样
比如一个叫 "topic",另一个叫 "topics",那就通信不了。
坑 2:消息类型不一样
比如一个发 String,另一个却按别的类型接收,也不行。
坑 3:QoS 不兼容
这是 ROS 2 里很常见的问题。
表面看起来都正常,但实际上两边连不上。
坑 4:忘了 spin
如果没有:
rclcpp::spin(...)
节点可能很快就退出了。
坑 5:编译后忘了 source install/setup.bash
这样系统可能找不到你刚刚编译出来的包。
十六、一句话总结
最后用一句最好记的话收尾:
发布者负责发消息,订阅者负责收消息,topic 是交流频道,QoS 决定消息按什么规则传。
如果你是刚入门,建议你先把下面这句背下来:
auto qos = rclcpp::QoS(10).reliable();
然后再记住:
create_publisher(...)
create_subscription(...)
这样你就已经把 ROS 2 发布订阅最核心的部分入门了。