ROS 2 里 rclcpp::QoS 到底是什么?一篇讲给零基础看的入门笔记
刚开始学 ROS 2 的时候,很多人都会碰到一个很奇怪的问题:
- topic 名字明明一样
- 消息类型也一样
- 发布者和订阅者都启动了
- 结果订阅端就是收不到消息
这时候,问题往往不在代码逻辑,而在 QoS。
而在 C++ 里,我们最常接触到的就是 rclcpp::QoS。
这篇文章不讲复杂理论,就用大白话把它讲明白。看完你至少能搞懂下面这些问题:
rclcpp::QoS是干什么的?- 为什么 ROS 2 需要它?
QoS(10)到底是什么意思?reliable()、best_effort()、transient_local()分别有什么区别?- 为什么有时候 topic 对上了,还是收不到消息?
一、先说人话:QoS 到底是什么?
QoS,全称是 Quality of Service,中文一般叫服务质量策略。
你可以把它理解成:
ROS 2 在传消息之前,先约定好“这批消息要怎么传”。
比如:
- 消息能不能丢?
- 要不要尽量保证每条都送到?
- 订阅者来晚了,还能不能拿到之前发过的消息?
- 最多缓存多少条消息?
- 多久没发消息算异常?
这些规则,加在一起,就是 QoS。
而 rclcpp::QoS,就是 ROS 2 C++ 里专门用来设置这些规则的类。
二、为什么 ROS 2 需要 QoS?
在 ROS 1 里,很多通信场景比较“固定”,开发者平时不用想太多。
但 ROS 2 面向的场景更复杂,比如:
- 机器人内部局域网通信
- Wi-Fi 环境下通信
- 高频传感器数据
- 实时控制
- 分布式系统
- 工业场景
不同场景,对通信的要求完全不一样。
举几个例子:
场景 1:激光雷达数据
激光雷达每秒发很多数据。
这时候你通常更关心的是:
我尽快拿到最新的一帧就行,丢掉几帧问题不大。
那就不一定要“强可靠”。
场景 2:机械臂控制指令
如果你发的是控制命令,比如“移动到某个位置”。
这时候你更关心的是:
这条指令最好别丢。
那就应该用更可靠的方式传输。
场景 3:地图、静态配置
比如你发布了一张地图,或者一个初始化参数。
你希望:
即使订阅者晚启动,也能拿到之前发过的内容。
那就需要“保存历史消息”的能力。
所以 ROS 2 设计了 QoS,让开发者可以按场景选择通信策略。
三、rclcpp::QoS 是什么?
rclcpp::QoS 是一个 C++ 类,用来配置 ROS 2 通信里的 QoS 策略。
它最常用于:
- publisher
- subscription
- service / client
最常见的写法就是:
auto qos = rclcpp::QoS(10);
这句代码的意思不是“随便写个 10”。
它真正表示的是:
历史策略使用
KeepLast,队列深度是 10。
也就是:
auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
这两句本质上是一样的。
四、先看一个最简单的例子
auto qos = rclcpp::QoS(10);
auto pub = this->create_publisher<std_msgs::msg::String>(
"chatter",
qos
);
auto sub = this->create_subscription<std_msgs::msg::String>(
"chatter",
qos,
[](std_msgs::msg::String::SharedPtr msg)
{
std::cout << "收到消息: " << msg->data << std::endl;
}
);
这段代码里,发布者和订阅者都用了同一个 qos。
这通常意味着:
- 保留最近 10 条消息
- 其他策略先用默认值
默认情况下,ROS 2 常见的发布/订阅 QoS 可以理解成:
KeepLast(10)ReliableVolatile
后面会一个一个解释。
五、把 QoS 想成“快递规则”
为了让零基础读者更容易理解,你可以把 topic 通信想成“寄快递”。
发布者 = 发件人
订阅者 = 收件人
消息 = 快递
QoS = 快递规则
比如:
- History / Depth:仓库最多存多少件快递
- Reliability:快递要不要尽量保证送到
- Durability:新来的收件人能不能拿到之前库存里的快递
- Deadline:多久不发货算异常
- Liveliness:发件人是不是还活着
这样一想,QoS 就不抽象了。
六、初学者最该掌握的 4 个核心策略
对于零基础来说,先把下面 4 个搞懂,已经够用了。
1)History:历史策略
History 决定的是:
消息要怎么存。
常见有两种:
KeepLast(N)
只保留最近 N 条消息。
比如:
rclcpp::QoS(10)
本质上就是:
rclcpp::QoS(rclcpp::KeepLast(10))
表示:
只保留最近 10 条。
这是最常见、最实用的方式。
KeepAll()
尽量保存所有消息。
例如:
auto qos = rclcpp::QoS(rclcpp::KeepAll());
它的意思是:
不主动只保留最近几条,而是尽量都存下来。
但这并不代表真的“无限保存”,因为底层中间件仍然会受资源限制。
所以初学者一般不建议一上来就用 KeepAll()。
2)Depth:队列深度
Depth 可以理解成:
消息队列最多放多少条。
它通常和 KeepLast(N) 配合使用。
例如:
auto qos = rclcpp::QoS(10);
这里的 10 就是 depth。
它的直觉含义是:
- 如果消息来得很快
- 订阅者处理得比较慢
- 那么队列里最多先堆 10 条
- 更老的消息可能会被新消息顶掉
3)Reliability:可靠性
Reliability 决定的是:
消息要不要尽量保证送达。
主要有两种:
reliable()
可靠传输。
意思是:
尽量保证消息送到。
适合:
- 控制命令
- 重要状态
- 不希望轻易丢失的数据
例如:
auto qos = rclcpp::QoS(10).reliable();
best_effort()
尽力而为。
意思是:
能送就送,送不到也不强求。
适合:
- 激光雷达
- 相机
- IMU
- 高频传感器数据
为什么传感器常用它?
因为高频数据里,最新的一帧通常比“补发旧数据”更重要。
例如:
auto qos = rclcpp::QoS(5).best_effort();
4)Durability:持久性
Durability 决定的是:
后来才加入的订阅者,能不能拿到之前发过的消息。
常见有两种:
durability_volatile()
易失型。
表示:
订阅者只能收到“它开始订阅之后”的新消息。
这是默认情况。
例如你先发了 3 条消息,过了几秒钟订阅者才启动。
如果是 volatile,那它拿不到之前那 3 条。
transient_local()
本地持久型。
表示:
发布者会保留一部分历史消息,后来加入的订阅者也有机会收到这些旧消息。
这个效果很像 ROS 1 里的 latch(锁存)。
特别适合:
- 地图
- 静态参数
- 初始化状态
- 一次发布,希望后来订阅者也能拿到
例如:
auto qos = rclcpp::QoS(1).transient_local().reliable();
这通常表示:
- 保留最近 1 条消息
- 可靠传输
- 晚加入的订阅者也能拿到这条历史消息
七、剩下几个策略,知道名字就行
除了上面那 4 个最常用的,还有一些稍微进阶一点的策略。
1)deadline()
表示:
两次消息之间允许的最大时间间隔。
如果超过这个时间还没等到新消息,就会被认为“超时”了。
适合做实时性检查。
2)lifespan()
表示:
一条消息最多活多久。
过期之后,即使它还没被处理,也会被当作无效消息。
适合对“新鲜度”要求高的场景。
3)liveliness()
表示:
系统怎么判断“这个发布者还活着”。
4)liveliness_lease_duration()
表示:
多久没证明自己还活着,就认为这个发布者失活了。
对于初学者来说,这几个先知道它们是“进阶实时/健康检测选项”就够了。
平时写普通节点,先不用在它们上面花太多精力。
八、rclcpp::QoS 常见写法汇总
最基础写法
auto qos = rclcpp::QoS(10);
适合刚开始学习时使用。
显式写出 KeepLast
auto qos = rclcpp::QoS(rclcpp::KeepLast(10));
和 QoS(10) 本质一样,只是写得更明确。
设置为可靠传输
auto qos = rclcpp::QoS(10).reliable();
设置为尽力而为
auto qos = rclcpp::QoS(5).best_effort();
设置为类似 ROS 1 latch 的效果
auto qos = rclcpp::QoS(1).transient_local().reliable();
保留所有历史消息
auto qos = rclcpp::QoS(rclcpp::KeepAll());
九、ROS 2 里一些常见的预定义 QoS
除了自己手动配置,ROS 2 还提供了一些预定义的 QoS 类型。
1)普通发布/订阅默认风格
auto qos = rclcpp::QoS(10);
常见理解就是:
KeepLast(10)ReliableVolatile
这是最常见的通用型配置。
2)传感器数据风格:SensorDataQoS
auto qos = rclcpp::SensorDataQoS();
它通常适合:
- 雷达
- 相机
- IMU
- 高频传感器
因为它更偏向:
- 小队列
best_effort- 追求“最新数据尽快到达”
例如:
auto sub = this->create_subscription<sensor_msgs::msg::LaserScan>(
"/scan",
rclcpp::SensorDataQoS(),
std::bind(&MyNode::scan_callback, this, std::placeholders::_1)
);
3)服务风格:ServicesQoS
auto qos = rclcpp::ServicesQoS();
服务一般更强调可靠性。
4)参数风格:ParametersQoS
auto qos = rclcpp::ParametersQoS();
参数通信通常会使用更大的队列深度,避免请求轻易丢失。
十、为什么有时候 topic 对上了,还是收不到消息?
这就是 QoS 最容易踩坑的地方。
在 ROS 2 里:
发布者和订阅者不是只要 topic 名和消息类型一致就一定能通信。
还要看:
QoS 是否兼容。
十一、QoS 兼容性:最重要的理解
ROS 2 的兼容模型可以简单记成一句话:
订阅者提要求,发布者看自己能不能满足。
意思就是:
- 订阅者说:我最低能接受什么质量
- 发布者说:我最多能提供什么质量
如果发布者满足不了订阅者的要求,连接就建立不起来。
十二、最常见的兼容性例子
例子 1:可靠性不兼容
| 发布者 | 订阅者 | 能否通信 |
|---|---|---|
best_effort | best_effort | 可以 |
best_effort | reliable | 不可以 |
reliable | best_effort | 可以 |
reliable | reliable | 可以 |
这里最容易记错的是这一条:
发布者是
best_effort,订阅者要求reliable,不兼容。
因为发布者给不了那么高的保证。
例子 2:持久性不兼容
| 发布者 | 订阅者 | 能否通信 |
|---|---|---|
volatile | volatile | 可以 |
volatile | transient_local | 不可以 |
transient_local | volatile | 可以 |
transient_local | transient_local | 可以 |
这说明:
如果订阅者要求“你得保存历史消息给我”,
但发布者本身根本不保存,那就没法通信。
十三、记住这几个最常见的使用场景
这一部分最适合拿来背。
场景 1:普通状态话题
比如:
- 机器人状态
- 任务状态
- 控制反馈
推荐:
auto qos = rclcpp::QoS(10).reliable();
场景 2:高频传感器
比如:
- 相机
- 激光雷达
- IMU
推荐:
auto qos = rclcpp::SensorDataQoS();
或者:
auto qos = rclcpp::QoS(5).best_effort();
场景 3:地图、静态配置、初始化消息
推荐:
auto qos = rclcpp::QoS(1).transient_local().reliable();
场景 4:刚开始学习,先别想太复杂
推荐先用:
auto qos = rclcpp::QoS(10);
等真正遇到“丢消息”“延迟”“晚加入收不到历史消息”等问题,再针对性调整。
十四、几个最常见的坑
坑 1:topic 名一样,不代表一定能通信
还要看 QoS 是否兼容。
坑 2:depth 不是越大越好
队列太大可能会堆积旧消息,导致处理到的数据不够“新”。
坑 3:传感器数据不一定适合 reliable
对高频传感器来说,“尽快拿到最新数据”往往比“每一帧都补回来”更重要。
坑 4:想让后来订阅者拿到旧消息,不能只靠 depth
你还得设置:
.transient_local()
否则只是普通缓存,不会像 ROS 1 的 latch 那样工作。
坑 5:durability_volatile() 这个名字别觉得奇怪
它不叫 volatile(),是因为 volatile 在 C++ 里本来就是关键字。