一. ROS2质量服务策略Qos
1. 我们在之前博客 基础上做一些实验,首先不修改Qos策略,运行发布端和订阅段的程序,效果如下:

我们改用命令行来发布话题数据
ros2 topic pub -r 2 /topic_demo1 std_msgs/msg/String "{data: \"hello world\"}"
命令行订阅话题数据
ros2 topic echo /topic_demo1
效果如下(因为发布端程序和命令行方式都在发送话题,所以订阅端,这两种话题数据都会收到):

如果运行订阅段程序,则效果如下(因为发布端程序和命令行方式都在发送话题,所以订阅端,这两种话题数据都会收到):

2. 接下里我们修改下发布端和订阅端代码
修改topic_demo1_publisher_side.py中代码如下:
python
# 导入rclpy库
import rclpy
from rclpy.node import Node
# 导入String字符串消息
from std_msgs.msg import String
# 导入QoS相关类
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
# 创建一个继承于Node基类的子类
class Topic_Pub(Node):
def __init__(self, name):
super().__init__(name)
# 1. 配置QoS策略:可靠传输,保留最后1条历史数据
self.qos_profile = QoSProfile(
reliability=QoSReliabilityPolicy.RELIABLE, # 可靠传输(重传丢失数据)
history=QoSHistoryPolicy.KEEP_LAST, # 保留最后N条数据
depth=1 # 保留1条历史数据
)
# 参数含义: 消息类型, "话题名", 消息队列长度
self.pub = self.create_publisher(String, "/topic_demo1", self.qos_profile)
#self.create_timer(1.0, self.timer_callback)
#意思:每隔1秒执行一次timer_callback
self.timer = self.create_timer(1, self.pub_msg)
def pub_msg(self):
msg = String() # 创建一个String类型的变量msg
msg.data = "Hi,I publish a message." # 给msg里边的data赋值
self.pub.publish(msg) # 发布话题数据
# 主函数
def main():
rclpy.init() # 初始化
pub_demo = Topic_Pub("topic_publisher_node")
rclpy.spin(pub_demo)
pub_demo.destroy_node() # 销毁节点对象
rclpy.shutdown() # 关闭ROS2 Python接口
修改topic_demo1_subscriber_side.py中代码如下:
python
#导入相关的库
import rclpy
from rclpy.node import Node
from std_msgs.msg import String
from rclpy.qos import QoSProfile, QoSReliabilityPolicy, QoSHistoryPolicy
class Topic_Sub(Node):
def __init__(self,name):
super().__init__(name)
#self.create_subscription(
# msg_type, # 1. 消息类型(必须)
# topic, # 2. 话题名称(必须)
# callback, # 3. 收到消息后的回调函数(必须)
# qos_profile, # 4. 队列长度(必须)
# callback_group=None, # 5. 回调组(可选)
#)
# 4个必须参数(你写代码必写)
# msg_type 消息类型,如String、Image、LaserScan
# topic 话题名字字符串,如"my_topic"
# callback 收到消息后自动调用的函数
# qos_profile队列长度,一般写10
# 1. 配置与发布者兼容的QoS策略
self.qos_profile = QoSProfile(
reliability=QoSReliabilityPolicy.BEST_EFFORT,
history=QoSHistoryPolicy.KEEP_LAST,
depth=1
)
self.sub = self.create_subscription(String,"/topic_demo1",self.sub_callback,self.qos_profile)
def sub_callback(self,msg):
# print(msg.data,flush=True)
self.get_logger().info(msg.data)
def main():
rclpy.init()
sub_demo = Topic_Sub("topic_subscriber_node") # 创建对象并进行初始化
rclpy.spin(sub_demo)
sub_demo.destroy_node() #销毁节点对象
rclpy.shutdown() #关闭ROS2 Python接口
然后执行colcon build,去运行发布端和订阅端程序
python
ros2 run pythonpackagedemo1 topic_demo1_publisher_side
ros2 run pythonpackagedemo1 topic_demo1_subscriber_side
执行效果如下:

执行命令查看话题详细 QoS 协商结果:
python
ros2 topic info /topic_demo1 --verbose

可看到整条链路降级为 BEST_EFFORT了,丢包不重传;
我们再接着将发布端reliability参数值改为QoSReliabilityPolicy.BEST_EFFORT,订阅端改为RELIABLE,更改完毕后,再编译,执行两个端的可执行程序。
发布端

订阅端

可看到,订阅端是收不到发布的数据的且有提示消息,如下,得修改代码,做到两者保持一致

注:也可以用命令行语句调用时,去做指定修改
命令行修改发布端和订阅端保持一致
python
ros2 topic pub -r 2 /topic_demo1 std_msgs/msg/String "{data: \"Hi,I publish a message.\"}" --qos-reliability reliable

或命令行修改订阅端和发布端保持一致
python
ros2 topic echo /topic_demo1 --qos-reliability best_effort

3. QoSProfile 完整可配置字段一共有 10 个:
- reliability
- history
- depth
- durability
- lifespan
- liveliness
- deadline
- liveliness_lease_duration
- max_blocking_time
- partition
1)核心枚举策略类参数(4 个,决定通信核心规则)
reliability 可靠性策略 ,控制消息的丢包重传规则 ,是决定通信是否可靠的核心参数,发布 / 订阅两端的取值会直接影响通信兼容性。可选值介绍参考AI工具里介绍
| 可选值 | 详细含义 | 适用场景 |
|---|---|---|
QoSReliabilityPolicy.RELIABLE |
可靠传输模式:发布方发送消息后,必须等待订阅方返回 ACK 确认包;网络丢包、延迟时会自动重发,保证订阅端 100% 收到消息 | 你的机器人控制指令(/robot_cmd)、导航目标点、启停命令、紧急停止等绝对不能丢失的关键消息 |
QoSReliabilityPolicy.BEST_EFFORT |
尽力传输模式:无 ACK 确认机制,发布方发完消息就结束,网络差直接丢包不重传,延迟更低但不保证送达 | 相机图像、激光雷达、IMU 等高频传感器数据流,允许少量丢包,优先保证低延迟 |
兼容规则如下:
发布 = RELIABLE, 订阅 = BEST_EFFORT:可建立通信,但整条链路降级为 BEST_EFFORT,丢包不重传;
发布 = BEST_EFFORT,订阅 = RELIABLE:完全匹配失败,订阅端收不到任何消息;
history 消息缓策略,控制发布 / 订阅端的消息缓冲区如何存放未处理的消息 ,必须搭配depth参数使用,决定了消息的覆盖规则。可选值介绍参考AI工具里介绍
| 可选值 | 详细含义 | 适用场景 |
|---|---|---|
QoSHistoryPolicy.KEEP_LAST |
仅保留最新的depth条消息,新消息进入缓冲区后,会直接覆盖最旧的消息,内存占用完全可控 |
你的机器人控制指令、实时传感器数据等只关心最新消息,过时消息无意义的场景,是 90% 场景的首选 |
QoSHistoryPolicy.KEEP_ALL |
缓存所有历史消息,无数量上限,只要缓冲区没满就不会丢弃任何消息 | 极少使用,仅适合日志、全量数据回放等必须保留所有历史数据的场景,风险是消息处理慢会导致内存持续暴涨 |
durability 消息持久化策略,控制后上线的订阅节点 ,能否收到发布节点在订阅启动前发送的历史消息,也就是常说的「晚加入者(Late Joiner)」处理规则。可选值介绍参考AI工具里介绍
| 可选值 | 详细含义 | 适用场景 |
|---|---|---|
QoSDurabilityPolicy.VOLATILE |
非持久化模式:订阅节点启动前发布的所有消息全部丢弃,订阅端只能接收上线后新发布的消息 | 你的机器人实时运动控制指令、高频传感器数据等旧指令无执行意义的场景,是系统默认值 |
QoSDurabilityPolicy.TRANSIENT_LOCAL |
本地持久化模式:发布节点会在本地缓存历史消息,新订阅节点连接后,会自动补发缓存内的所有消息 | 静态地图、参数配置、界面状态、固定初始参数等新节点上线需要先获取当前最新状态的场景 |
liveliness 节点活性保活策略,用于检测发布节点是否在线、崩溃、卡死 ,订阅端可以通过该策略的回调,及时感知发布节点的离线状态,做异常处理。可选值介绍参考AI工具里介绍
| 可选值 | 详细含义 | 适用场景 |
|---|---|---|
QoSLivelinessPolicy.AUTOMATIC |
自动保活模式:DDS 底层会自动维护心跳包,无需用户代码干预,自动检测节点是否在线 | 绝大多数常规场景的首选,你的机器人控制指令场景用这个就足够 |
QoSLivelinessPolicy.MANUAL_BY_TOPIC |
手动保活模式:需要用户代码主动调用publish()发送消息,才能维持节点活性;长时间不发送消息,就会被判定为离线 |
仅适合需要手动控制节点生命周期的进阶场景,比如低功耗设备、按需唤醒的节点 |
2)数值类参数(1 个,控制缓冲区大小)
depth 消息队列深度,配合history=KEEP_LAST使用,定义消息缓冲区的最大存储消息条数 ,是控制内存占用的核心参数。类型:int 正整数,取值≥1;当history=KEEP_ALL时,该参数完全无效,不会限制队列大小;取值越大,能缓存的历史消息越多,内存占用也越高。
适用场景:你的机器人控制指令:depth=1 最优,只保留最新 1 条指令,旧指令直接被覆盖,避免执行滞后的错误指令;传感器数据:depth=5~10 即可,平衡内存占用和数据容错。
lifespan 消息生命周期,定义单条消息的有效期 ,消息从发布时刻开始计时,超过该时长未被订阅端处理,会直接被丢弃,不会再执行。类型:Duration,取值≥0;取值为 0 时,消息永远不会过期,直到被处理或覆盖。适用场景:你的机器人控制指令:建议设为Duration(seconds=2.0),2 秒前的旧指令自动作废,避免卡顿后执行过时指令;
deadline 消息周期时限,约定该话题的最大消息发布间隔 ,用于检测发布节点是否卡死、断流:发布端:两次发布消息的间隔不能超过该时长;订阅端:超过该时长未收到新消息,会触发「消息丢失」事件,可通过回调捕获。类型:Duration,取值 > 0;取值越小,对发布端的实时性要求越高。适用场景:你的机器人控制指令:如果要求 1 秒必须发一次指令,设为Duration(seconds=1.0),超时未收到可触发紧急停止;传感器数据:比如 IMU 要求 100Hz 发布,设为Duration(milliseconds=10),超时可判定传感器掉线。
liveliness_lease_duration 活性超时时间, 配合liveliness策略使用,定义多久没检测到发布者的心跳,就判定发布节点离线 。类型:Duration,取值 > 0;必须和liveliness策略搭配使用,单独设置无意义。适用场景:你的机器人控制指令:设为Duration(seconds=3.0),3 秒没收到发布者心跳,就判定控制节点离线,可触发紧急停止;常规场景:建议设为发布周期的 2~3 倍,避免误判。
max_blocking_time 最大阻塞等待时间,定义发布缓冲区满时,publish () 方法最多阻塞等待的时长 ,超时仍无法写入缓冲区,就会发布失败,消息直接丢弃。类型:Duration,取值≥0;取值为 0 时,缓冲区满就直接丢弃消息,不阻塞。适用场景:高频发布场景:比如相机图像高频发布,缓冲区偶尔满时,可设为Duration(milliseconds=50),给一点等待时间,避免频繁丢包;你的机器人控制指令:用默认值即可,控制指令发布频率低,缓冲区几乎不会满。
3)分组隔离类参数(1 个,实现多节点通信隔离)
partition 话题分区标签,实现同一话题下的多组隔离通信 ,只有发布端和订阅端的partition列表中存在相同的字符串,才能建立通信,互不干扰。类型:list[str],字符串列表,可填多个分区标签;空列表[]为默认值,属于默认公共分区,所有同话题节点都能互通。适用场景:多机器人集群场景:比如机器人 A 的partition=["robot_01"],机器人 B 的partition=["robot_02"],同是/robot_cmd话题,两个机器人不会收到对方的指令;多模块隔离:比如导航模块和底盘模块用不同分区,避免指令串扰。
4. 常见注意事项:
QoS 匹配是核心 :发布端和订阅端的所有枚举参数必须完全一致,否则会出现协商降级、甚至收不到消息的问题;
depth 和 history 必须搭配 :只有history=KEEP_LAST时,depth 才有效,不要设了 KEEP_ALL 还改 depth;
时间参数不要设的过短:比如 deadline 设的比发布周期还短,会频繁触发断流误判;
分区不要乱填:一旦填了 partition,订阅端必须有相同的标签才能收到消息,否则会完全收不到。
二. 时间相关API
我们拿如下博客中的例子来做实验
ROS工作空间、功能包、节点创建_创建自己的功能包与节点-CSDN博客
编辑pythonexecute1.py文件代码如下:
python
import rclpy # ROS2 Python接口库
from rclpy.node import Node # ROS2 节点类
import time
import threading
from rclpy.time import Time
from rclpy.duration import Duration
"""
创建一个py_hello节点
"""
class pyHelloNode1(Node):
def __init__(self, name):
super().__init__(name)
# 计数器,用于演示定时器执行次数
self.counter = 0
# 创建定时器:每1秒执行一次callback函数
self.timer = self.create_timer(2.0, self.timer_callback)
# 创建一个更快的定时器:每2秒执行一次
self.fast_timer = self.create_timer(1.0, self.fast_timer_callback)
self.get_logger().info("定时器节点已启动")
def get_time(self):
return self.get_clock().now()
def timer_callback(self):
"""2秒定时器回调函数"""
self.counter += 1
current_time = self.get_clock().now()
# 打印当前时间和计数器值
self.get_logger().info(
f"[2秒定时器] 第 {self.counter} 次执行,当前时间: {current_time.seconds_nanoseconds()}")
def fast_timer_callback(self):
"""1秒定时器回调函数"""
# 打印当前时间戳(纳秒)
self.get_logger().info(
f"[1秒定时器] 当前时间戳: {self.get_clock().now().nanoseconds}")
# ROS2节点父类初始化
# while rclpy.ok(): # ROS2系统是否正常运行
# self.get_logger().info("Hello python node1") # ROS2日志输出
# time.sleep(0.5) # 休眠控制循环时间
def run_loop(self):
# 使用节点的create_rate()创建2Hz的Rate
rate = self.create_rate(2)
count = 0
try:
while rclpy.ok():
self.get_logger().info(f"循环执行 {count} 次")
count += 1
rate.sleep() # 休眠到下一个周期(0.5秒)
except KeyboardInterrupt:
self.get_logger().info("循环被中断")
def main(args=None):
# ROS2节点主入口main函数
rclpy.init(args=args) # ROS2 Python接口初始化
node = pyHelloNode1("Hello_python_node1") # 创建ROS2节点对象并进行初始化
# time类的使用方法,创建'时间点、时刻'
time1_demo = Time(seconds=1)
current_time = node.get_time()
time.sleep(5)
duration1 = node.get_time() - current_time
t2 = time1_demo + duration1
node.get_logger().info(f'duration1: {duration1}')
node.get_logger().info("t2 = %d" % t2.nanoseconds)
# 创建线程运行循环(避免阻塞主线程)
loop_thread = threading.Thread(target=node.run_loop)
loop_thread.start()
# 主线程执行spin,维持ROS 2节点运行
try:
rclpy.spin(node)
except KeyboardInterrupt:
pass
finally:
loop_thread.join() # 等待线程结束
node.destroy_node()
rclpy.shutdown()
然后重新colcon build, 执行该可执行程序,效果如下:
python
ros2 run pythonpackagedemo1 pythonexecute_onenode

此篇到此结束,我们再继续深入!