4. 话题通信基础:话题模型、发布者与订阅者、消息类型

4.1 话题通信的核心模型

话题(Topic)通信是ROS2中最基础、最常用的数据交换方式,其本质是一种异步、单向、多对多的发布/订阅模式。与ROS1不同,ROS2的话题通信基于DDS(Data Distribution Service)中间件实现,去掉了中心化的Master节点,节点之间通过DDS的发现机制自动建立连接。

话题通信模型包含三个核心角色:

  • 发布者(Publisher):负责向指定话题发送消息,不关心谁在接收。
  • 订阅者(Subscriber):负责监听指定话题并接收消息,不关心消息从哪来。
  • 话题(Topic):发布者与订阅者之间的“数据管道”,以字符串名称标识,并绑定一种固定的消息类型。

下图展示了ROS2话题通信的典型结构,其中发布者节点将消息推送到话题,订阅者节点从话题中拉取消息,整个过程无需双方直接建立连接。

发布者节点 /camera_node Publisher 话题 Topic /image_raw 消息类型: sensor_msgs/Image DDS数据通道 订阅者节点 /image_viewer Subscriber 发布 订阅 发布者与订阅者通过DDS发现机制自动匹配,无需知道对方地址 同一话题可同时存在多个发布者和多个订阅者(多对多)
关键理解:话题通信是单向数据流。发布者只管“发”,订阅者只管“收”,双方不感知对方的存在。如果需要请求-应答式的双向通信,应使用服务(Service)或动作(Action)机制,而非话题。

4.2 消息类型(Message Type)

话题上传输的数据必须遵循预定义的结构化格式,即消息类型。消息类型由包名/消息名组成,例如 std_msgs/msg/String、geometry_msgs/msg/Twist。ROS2中消息类型定义在 .msg 文件中,每个字段由“类型 + 字段名”构成。

常用消息类型示例:

消息类型 所属包 典型字段 应用场景
std_msgs/msg/String std_msgs string data 文本日志、简单状态通知
geometry_msgs/msg/Twist geometry_msgs Vector3 linear, Vector3 angular 机器人速度控制指令
sensor_msgs/msg/LaserScan sensor_msgs float32[] ranges, float32 angle_min 激光雷达数据
nav_msgs/msg/Odometry nav_msgs PoseWithCovariance pose, TwistWithCovariance twist 机器人里程计信息

查看消息类型的详细结构,可在终端使用命令:

ros2 interface show geometry_msgs/msg/Twist
# 输出示例:
# Vector3  linear
# Vector3  angular

4.3 编写发布者与订阅者(Python实现)

下面通过一个完整的示例演示如何创建发布者和订阅者节点。假设我们要发布一个字符串话题 /hello_topic,消息类型为 std_msgs/msg/String。

4.3.1 创建发布者节点

# publisher_node.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class SimplePublisher(Node):
    def __init__(self):
        super().__init__('simple_publisher')
        # 创建发布者:话题名 /hello_topic,消息类型 String,队列长度 10
        self.publisher_ = self.create_publisher(String, '/hello_topic', 10)
        self.timer = self.create_timer(1.0, self.timer_callback)  # 每秒触发一次
        self.count = 0

    def timer_callback(self):
        msg = String()
        msg.data = f'Hello ROS2: {self.count}'
        self.publisher_.publish(msg)
        self.get_logger().info(f'发布: {msg.data}')
        self.count += 1

def main(args=None):
    rclpy.init(args=args)
    node = SimplePublisher()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

4.3.2 创建订阅者节点

# subscriber_node.py
import rclpy
from rclpy.node import Node
from std_msgs.msg import String

class SimpleSubscriber(Node):
    def __init__(self):
        super().__init__('simple_subscriber')
        # 创建订阅者:话题名 /hello_topic,消息类型 String,队列长度 10
        self.subscription = self.create_subscription(
            String, '/hello_topic', self.listener_callback, 10)

    def listener_callback(self, msg):
        self.get_logger().info(f'收到: {msg.data}')

def main(args=None):
    rclpy.init(args=args)
    node = SimpleSubscriber()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

4.3.3 运行与验证

分别打开两个终端,先运行发布者,再运行订阅者:

# 终端1
python3 publisher_node.py

# 终端2
python3 subscriber_node.py

此时订阅者终端会持续打印收到的消息。也可以使用ROS2命令行工具直接查看话题数据:

ros2 topic list                    # 查看所有活跃话题
ros2 topic echo /hello_topic       # 监听并打印话题内容
ros2 topic info /hello_topic       # 查看话题类型、发布者/订阅者数量

4.4 队列长度与QoS策略

在创建发布者或订阅者时,最后一个参数队列长度(Queue Size)并非随意设置,它直接影响通信的可靠性。队列长度表示在接收方来不及处理时,ROS2在内存中暂存多少条消息。

  • 队列长度 = 10:适合传感器数据流(如图像、激光),允许丢弃旧数据,保证实时性。
  • 队列长度 = 100 或更大:适合低频但重要的控制指令或日志,避免丢失。
  • 队列长度 = 1:只保留最新一条消息,适用于实时性要求极高的场景。

ROS2还提供了更精细的QoS(Quality of Service)策略,用于控制消息的可靠性(Reliable vs Best Effort)、历史记录策略等。例如,传感器数据通常使用 Best Effort 模式,而控制指令使用 Reliable 模式:

from rclpy.qos import QoSProfile, ReliabilityPolicy

qos = QoSProfile(depth=5, reliability=ReliabilityPolicy.BEST_EFFORT)
self.subscription = self.create_subscription(
    String, '/hello_topic', self.callback, qos)
常见误区:发布者和订阅者的QoS策略必须兼容才能建立通信。例如,发布者使用 Reliable,订阅者使用 Best Effort,则通信会降级为 Best Effort;若发布者使用 Best Effort,订阅者使用 Reliable,则通信将无法建立。在调试话题不通时,优先检查双方的QoS设置。

4.5 案例:控制机器人运动

下面结合一个实际场景,演示话题通信在机器人控制中的应用。假设我们有一个差速驱动机器人,需要发布速度指令到 /cmd_vel 话题,消息类型为 geometry_msgs/msg/Twist。

# cmd_vel_publisher.py
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist

class CmdVelPublisher(Node):
    def __init__(self):
        super().__init__('cmd_vel_publisher')
        self.publisher_ = self.create_publisher(Twist, '/cmd_vel', 10)
        self.timer = self.create_timer(0.1, self.publish_cmd)  # 10Hz

    def publish_cmd(self):
        twist = Twist()
        twist.linear.x = 0.2    # 前进速度 0.2 m/s
        twist.angular.z = 0.5   # 角速度 0.5 rad/s
        self.publisher_.publish(twist)

def main(args=None):
    rclpy.init(args=args)
    node = CmdVelPublisher()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()

在真实机器人或仿真环境中,底盘驱动节点会订阅 /cmd_vel 话题,将速度指令转换为轮子转速。这种“发布指令-执行机构订阅”的模式是ROS2机器人应用中最常见的架构。

4.6 误区与复盘

常见误区 问题描述 正确做法
话题名拼写不一致 发布者使用 /cmd_vel,订阅者使用 /cmdvel,导致收不到数据 使用 ros2 topic list 确认实际话题名
消息类型不匹配 发布 Twist,订阅 String,编译不报错但运行时不通信 用 ros2 topic info /topic 查看类型是否一致
忘记调用 rclpy.spin() 回调函数不执行,订阅者收不到消息 确保节点进入事件循环,或使用 MultiThreadedExecutor
QoS不兼容 发布者 Best Effort,订阅者 Reliable,连接失败 统一QoS策略,或使用默认的 Reliable

复盘建议:当话题通信出现问题时,按照以下顺序排查:① ros2 topic list 确认话题存在;② ros2 topic info 确认类型匹配;③ ros2 topic echo 确认数据在流动;④ 检查两端节点的QoS策略;⑤ 确认节点都在运行且没有崩溃。

话题通信是ROS2机器人开发的地基,掌握发布者、订阅者、消息类型三要素,并理解队列与QoS的影响,后续学习服务、动作、参数等机制时就能触类旁通。