ROS 2 里 rclcpp::QoS到底是什么?一篇讲给零基础看的入门笔记

5 阅读10分钟

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)
  • Reliable
  • Volatile

后面会一个一个解释。


五、把 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)
  • Reliable
  • Volatile

这是最常见的通用型配置。


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_effortbest_effort可以
best_effortreliable不可以
reliablebest_effort可以
reliablereliable可以

这里最容易记错的是这一条:

发布者是 best_effort,订阅者要求 reliable,不兼容。

因为发布者给不了那么高的保证。


例子 2:持久性不兼容

发布者订阅者能否通信
volatilevolatile可以
volatiletransient_local不可以
transient_localvolatile可以
transient_localtransient_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++ 里本来就是关键字。