适用版本:ROS 2 Humble / Jazzy / Rolling  |  预计阅读:25 分钟  |  前置知识:已完成 ROS 2 安装,了解"节点(Node)"的基本概念


想象一下:你打开微信,进入一个群聊。群里有人发了一条消息——"今晚开会"。发消息的人不需要知道谁在线、谁会看到;而你作为群成员,也不需要知道这条消息具体是谁发的。你只管收,他只管发,彼此互不干扰。

这就是 ROS 2 中话题(Topic)通信的核心思想。

在机器人系统里,激光雷达不停地广播扫描数据,控制器不停地读取这些数据并计算运动指令,电机再根据指令转动轮子——它们之间的数据流转,正是通过"话题"这一机制来完成的。理解话题,是掌握 ROS 2 分布式通信架构的第一步,也是最重要的一步。

本教程将从概念到命令行、从原理到代码,带你系统性地掌握 ROS 2 话题通信的核心知识与实战技能。


1. 什么是 Topic?—— 核心概念解析

1.1 发布-订阅模型:机器人世界的"微信群聊"

ROS 2 的话题通信采用的是经典的发布-订阅(Publish-Subscribe)模型。在这个模型中有两个角色:

发布者(Publisher)就像群聊中发消息的人:它把数据打包成消息,发送到一个指定名称的话题上。发布者完全不关心有没有人在听——即使没有任何订阅者,它依然照常发布。

订阅者(Subscriber)则像群聊中潜水看消息的人:它订阅某个话题后,每当有新消息到达,就会触发一个回调函数来处理数据。订阅者也不需要知道消息是谁发的、有几个发布者在发。

这种设计带来了一个非常重要的工程特性——松耦合(Loose Coupling)。发布者和订阅者之间没有任何直接依赖关系,它们唯一的"约定"就是话题名称和消息类型。这意味着你可以随时增加、删除或替换任何一个节点,而不影响系统的其他部分。想象一下:你可以在不改动任何代码的情况下,把一台机器人的激光雷达从 A 品牌换成 B 品牌——只要新雷达的驱动节点仍然向同一个话题发布相同类型的消息,整个系统就能无缝运行。

1.2 话题的三大构成要素

要真正理解话题,需要抓住三个核心要素:

要素一:Topic 名称(Name)——数据的唯一标识。每个话题都有一个全局唯一的路径名,以正斜杠 / 开头,例如 /cmd_vel(速度指令)、/scan(激光雷达扫描)、/turtle1/pose(小海龟的位姿)。话题名称是节点之间"找到彼此"的关键。就像微信群有不同的群名一样,节点通过话题名称来决定"我要往哪个频道发消息"或"我要监听哪个频道"。值得注意的是,ROS 2 支持命名空间(Namespace)机制——/robot1/cmd_vel 和 /robot2/cmd_vel 是两个完全独立的话题,这在多机器人项目中极为常见。

要素二:消息类型(Message Type)——传输的数据格式。每条消息都有严格的数据结构定义,例如 geometry_msgs/msg/Twist 包含线速度和角速度两个三维向量,sensor_msgs/msg/LaserScan 包含距离数组和角度范围等。发布者和订阅者必须使用完全相同的消息类型,否则通信将无法建立——就像两个人一个说中文、一个说法语,即使在同一间屋子里也听不懂对方。

要素三:通信方向——单向数据流。话题通信是单向的:数据从发布者流向订阅者,没有请求-响应的概念(那是"服务(Service)"的工作)。但这种单向流可以灵活组合:

通信模式 典型场景 示例
1 对 1 一个雷达 → 一个导航节点 /scan
1 对多 一个里程计 → 多个消费者 /odom 同时被导航、可视化、记录节点订阅
多对 1 多个传感器 → 一个融合节点 多个摄像头数据汇入感知模块
多对多 分布式系统中的自由组合 多个遥操作节点控制多个机器人

2. 命令行初体验:不写代码看懂 Topic

本节目标:学会使用 ROS 2 自带的命令行工具来观察和理解话题通信,建立直觉。

2.1 启动测试环境

打开两个终端窗口,分别运行以下命令:

# 终端 1:启动 turtlesim 仿真器
ros2 run turtlesim turtlesim_node

# 终端 2:启动键盘遥控节点
ros2 run turtlesim turtle_teleop_key

此时你应该能看到一个蓝色背景的小海龟窗口。在终端 2 中按方向键,小海龟就会移动起来。

现在问题来了:键盘节点是怎么让小海龟动起来的?答案就是——话题。键盘节点是发布者,小海龟是订阅者,它们通过 /turtle1/cmd_vel 这个话题进行通信。

2.2 常用调试命令速查

接下来我们像"调试工程师"一样,用命令行工具来窥探话题通信的全貌。

ros2 topic list — 查看当前活跃的"广播频道"

打开一个新终端(确保已 source ROS 2 环境),运行:

ros2 topic list

输出类似:

/parameter_events /rosout /turtle1/cmd_vel /turtle1/color_sensor /turtle1/pose

这就是当前系统中所有活跃的话题列表。/rosout 是日志话题,/parameter_events 是参数变更通知——它们由系统自动创建。我们最关心的是 /turtle1/cmd_vel(速度指令)和 /turtle1/pose(位姿反馈)。

ros2 topic info <topic_name> — 查看频道的详细信息

ros2 topic info /turtle1/cmd_vel

输出:

Type: geometry_msgs/msg/Twist Publisher count: 1 Subscription count: 1

这条命令告诉我们三件事:消息类型是 Twist,有 1 个发布者(键盘节点),有 1 个订阅者(turtlesim 节点)。如果你在发布命令时同时运行了 ros2 topic pub,你会看到 Publisher count 变成 2。

ros2 topic echo <topic_name> — 实时监听数据流

ros2 topic echo /turtle1/pose

此时在键盘控制终端里按方向键让小海龟移动,你会看到一连串位姿数据不断打印出来:

---
x: 5.544444561004639
y: 5.544444561004639
theta: 0.0
linear_velocity: 0.0
angular_velocity: 0.0
---

按 Ctrl+C 退出监听。

ros2 topic hz <topic_name> — 测量发布频率

ros2 topic hz /turtle1/pose

输出类似:

average rate: 62.500 min: 0.016s max: 0.016s std dev: 0.00001s window: 63

turtlesim 默认以约 62.5 Hz 的频率发布位姿。这个命令在调试传感器时特别有用——比如你的激光雷达标称 10 Hz,但 ros2 topic hz 显示只有 3 Hz,那就说明有性能瓶颈需要排查。

ros2 topic bw <topic_name> — 查看带宽占用

ros2 topic bw /turtle1/pose

这会显示消息的平均带宽占用,在评估高分频传感器数据(如点云、图像)时非常实用。

2.3 手动发送指令:让乌龟动起来

最令人兴奋的莫过于自己当一回"发布者"。先关闭键盘控制节点,然后运行:

ros2 topic pub --once /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0}, angular: {z: 1.8}}"

你应该会看到小海龟突然动了一下!这条命令的含义拆解如下:

版本提示:在部分较新的 ROS 2 版本中,--once 参数可能已被替换为 -1。如遇报错,请运行 ros2 topic pub --help 查看当前版本支持的参数格式。

参数 含义
--once 只发布一次就退出(不加则持续以 1 Hz 发布)
/turtle1/cmd_vel 目标话题名称
geometry_msgs/msg/Twist 消息类型(必须与话题匹配)
"{linear: {x: 2.0}, ...}" 消息内容,采用 YAML 格式

想让小海龟持续画圆?去掉 --once,把参数改成:

ros2 topic pub /turtle1/cmd_vel geometry_msgs/msg/Twist "{linear: {x: 2.0}, angular: {z: 1.8}}"

按 Ctrl+C 停止发布,小海龟就会停下来。

动手练习:尝试用 ros2 topic echo /turtle1/pose 在另一个终端观察小海龟的坐标变化,体会"数据从发布者流向订阅者"的过程。


3. 核心原理揭秘:Topic 是如何工作的?

3.1 DDS:话题通信的幕后英雄

在 ROS 1 时代,话题通信依赖一个叫做 Master 的中心节点。每个节点启动时都要先向 Master 注册,Master 负责牵线搭桥——这就像早期的电话交换机,你得先打给总机,总机才能帮你接通对方。这种架构有一个致命弱点:Master 一旦宕机,整个系统就瘫痪了。

ROS 2 做了一个大胆的决定:抛弃 Master,改用 DDS(Data Distribution Service,数据分发服务) 作为底层通信中间件。DDS 是一种经过工业验证的分布式通信标准,广泛应用于航空航天、国防、工业自动化等领域。

DDS 带来的最大改变是自动发现(Auto-Discovery)。每个节点启动后,会通过多播(multicast)自动广播自己的存在,并发现网络中的其他节点。这意味着:

  • 没有单点故障:不存在中心节点,任何一个节点挂掉都不影响其他节点的通信。
  • 即插即用:新节点加入后自动被发现,无需手动配置。
  • 天生支持分布式:多台电脑只要在同一网络中,节点就能自动互相发现并通信。

ROS 2 支持多种 DDS 实现(称为 RMW 中间件),包括 eProsima Fast DDS、Eclipse Cyclone DDS、Connext DDS 等。默认使用的是 Fast DDS,你也可以通过环境变量切换。

3.2 QoS(服务质量):ROS 2 的杀手级特性

如果 DDS 是 ROS 2 通信的"高速公路",那么 QoS(Quality of Service,服务质量) 就是这条公路上的"交通规则"。QoS 允许你根据数据的特性,精细地调整通信行为。这是 ROS 2 相比 ROS 1 最显著的架构优势之一。

让我们用通俗的语言来理解几个最重要的 QoS 策略:

可靠性(Reliability)——数据能不能丢?

想象两种场景:激光雷达每秒发出几千个距离测量点,偶尔丢一两帧完全没问题,因为下一帧马上就来;但如果你发送的是"紧急停车"指令,这条消息绝对不能丢。

  • Best Effort(尽力而为):发出去就不管了,允许在拥塞时丢包。优点是延迟低、性能好。适合传感器数据流。
  • Reliable(可靠传输):确保每一条消息都能送达,丢失会自动重传。适合控制指令、状态变更等关键数据。

持久性(Durability)——后来的订阅者能收到旧数据吗?

假设地图数据在系统启动时就发布了,但你的导航节点 30 秒后才启动。如果是"易失性"策略,导航节点永远收不到之前发布的地图;如果是"持久"策略,发布者会为新来的订阅者重新发送最后一条保留的数据。

  • Volatile(易失性):不保留旧数据,订阅者只能收到订阅之后发布的消息。
  • Transient Local(本地持久):发布者会为后来加入的订阅者保留最近的数据。

历史深度(History Depth)——队列里缓存几条?

  • Keep Last (N):只保留最近 N 条消息,旧的自动丢弃。
  • Keep All:保留所有未投递的消息(注意内存占用)。

ROS 2 预定义了几个常用的 QoS 配置文件(参见官方文档 [1]):

配置文件 可靠性 持久性 典型用途
Default Reliable Volatile 通用场景
Sensor Data Best Effort Volatile 摄像头、激光雷达等传感器
Services Reliable Volatile 服务调用
Parameters Reliable Volatile 参数服务

QoS 兼容性规则——为什么你的话题可能"连不上"?

这是初学者最常踩的坑之一。发布者和订阅者之间的 QoS 必须兼容才能建立连接。基本规则是:订阅者可以"请求"比发布者"提供"的更低或相等的服务质量,但不能更高。

具体来说:如果发布者使用 Best Effort,而订阅者请求 Reliable,那么连接不会建立——因为发布者无法提供订阅者所要求的可靠保证。反过来,如果发布者提供 Reliable,订阅者请求 Best Effort,则可以正常通信。

经验法则:如果你发现 ros2 topic info 显示有发布者和订阅者,但 ros2 topic echo 收不到数据,第一时间检查双方的 QoS 策略是否兼容。


4. 动手实战:编写你的第一个 Topic 节点

本节以 Python 为主要语言,代码注释兼顾中文说明,力求降低阅读门槛。

4.1 准备工作:创建工作空间和功能包

# 创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src

# 创建 Python 功能包
ros2 pkg create --build-type ament_python --license Apache-2.0 topic_tutorial \
  --dependencies rclpy geometry_msgs turtlesim

# 进入功能包目录
cd topic_tutorial

编辑 package.xml,确认依赖项已正确声明:

<exec_depend>rclpy</exec_depend>
<exec_depend>geometry_msgs</exec_depend>
<exec_depend>turtlesim</exec_depend>

编辑 setup.py,补全 data_files 配置(ros2 pkg create 通常会自动生成,请确认其完整性),并在 entry_points 中添加我们将在本节创建的节点入口:

import os
from glob import glob
from setuptools import find_packages, setup

package_name = 'topic_tutorial'

setup(
    name=package_name,
    version='0.1.0',
    packages=find_packages(exclude=['test']),
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='your_name',
    maintainer_email='your@email.com',
    description='ROS2 Topic tutorial examples',
    license='Apache-2.0',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'circle_driver = topic_tutorial.circle_publisher:main',
            'pose_monitor = topic_tutorial.pose_subscriber:main',
        ],
    },
)

重要:data_files 部分不可省略,否则 ros2 run 将无法发现你的功能包。同时请确认 resource/topic_tutorial 文件存在(ros2 pkg create 会自动创建)以及 topic_tutorial/topic_tutorial/__init__.py 文件存在。

4.2 编写发布者(Publisher):驱动小海龟画圆

在 topic_tutorial/topic_tutorial/ 目录下创建文件 circle_publisher.py:

"""
circle_publisher.py —— 定时向 /turtle1/cmd_vel 发布速度指令,驱动小海龟画圆。

代码结构拆解:
  1. 导入依赖 → 2. 定义节点类 → 3. 初始化节点
  4. 创建发布者 → 5. 设置定时器 → 6. 填充消息并发布
"""
import rclpy
from rclpy.node import Node
from geometry_msgs.msg import Twist


class CircleDriver(Node):
    """一个让小海龟做圆周运动的发布者节点。"""

    def __init__(self):
        # ① 初始化节点,命名为 'circle_driver'
        super().__init__('circle_driver')

        # ② 创建发布者对象
        #    - 话题名称: /turtle1/cmd_vel
        #    - 消息类型: Twist(包含线速度和角速度)
        #    - 队列深度: 10(QoS history depth)
        self.publisher_ = self.create_publisher(
            Twist,
            '/turtle1/cmd_vel',
            10
        )

        # ③ 创建定时器:每 100ms 触发一次(10 Hz)
        self.timer = self.create_timer(0.1, self.timer_callback)

        self.get_logger().info('Circle Driver 节点已启动,小海龟即将画圆 🐢')

    def timer_callback(self):
        """定时器回调:每次触发时构造并发布一条速度指令。"""
        msg = Twist()

        # 线速度 x 方向 = 2.0 m/s(前进)
        msg.linear.x = 2.0
        # 角速度 z 方向 = 1.8 rad/s(左转)
        msg.angular.z = 1.8

        # ④ 发布消息
        self.publisher_.publish(msg)
        self.get_logger().info(
            f'发布速度 → linear.x={msg.linear.x}, angular.z={msg.angular.z}'
        )


def main(args=None):
    rclpy.init(args=args)           # 初始化 ROS 2 通信
    node = CircleDriver()           # 实例化节点
    rclpy.spin(node)                # 阻塞运行(处理定时器与回调)
    node.destroy_node()             # 清理
    rclpy.shutdown()


if __name__ == '__main__':
    main()

代码要点解读

create_publisher(Twist, '/turtle1/cmd_vel', 10) 中的第三个参数 10 是 QoS 的历史深度,表示队列中最多缓存 10 条未发出的消息。create_timer(0.1, callback) 设置了一个每 0.1 秒触发一次的定时器,保证了速度指令的持续发布。Twist 消息的 linear.x 控制前进速度,angular.z 控制旋转速度——两者同时为正值时,小海龟就会走出一个圆弧。

4.3 编写订阅者(Subscriber):实时打印小海龟坐标

在 topic_tutorial/topic_tutorial/ 目录下创建文件 pose_subscriber.py:

"""
pose_subscriber.py —— 订阅 /turtle1/pose,实时打印小海龟的位置和朝向。

代码结构拆解:
  1. 导入依赖 → 2. 定义节点类 → 3. 初始化节点
  4. 创建订阅者 → 5. 编写回调函数处理接收到的数据
"""
import rclpy
from rclpy.node import Node
from turtlesim.msg import Pose


class PoseMonitor(Node):
    """一个监听小海龟位姿的订阅者节点。"""

    def __init__(self):
        # ① 初始化节点
        super().__init__('pose_monitor')

        # ② 创建订阅者对象
        #    - 话题名称: /turtle1/pose
        #    - 消息类型: Pose
        #    - 回调函数: self.pose_callback
        #    - 队列深度: 10
        self.subscription = self.create_subscription(
            Pose,
            '/turtle1/pose',
            self.pose_callback,
            10
        )
        # 防止 Python 的未使用变量警告
        self.subscription

        self.get_logger().info('Pose Monitor 节点已启动,正在监听小海龟位姿...')

    def pose_callback(self, msg: Pose):
        """每当收到一条位姿消息时触发。"""
        self.get_logger().info(
            f'位置 → x={msg.x:.2f}, y={msg.y:.2f}, '
            f'朝向 θ={msg.theta:.2f} rad'
        )


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


if __name__ == '__main__':
    main()

代码要点解读

订阅者的核心是回调函数 pose_callback。当新消息到达时,rclpy.spin() 会自动调用这个函数。你不需要写任何轮询或等待的逻辑——这就是"事件驱动"编程的魅力。Pose 消息包含 x、y(位置)、theta(朝向角度)、linear_velocity(线速度)和 angular_velocity(角速度)五个字段。

注意:这里使用的是 turtlesim.msg.Pose——turtlesim 包自定义的消息类型,它与标准的 geometry_msgs/msg/Pose(包含 position 和 orientation 字段)是不同的类型,在实际项目中请注意区分。

4.4 编译与运行

黄金法则:每打开一个新的终端窗口,第一件事就是执行 source install/setup.bash(在 ~/ros2_ws 目录下),否则 ROS 2 将找不到你的功能包和节点。

# 回到工作空间根目录
cd ~/ros2_ws

# 编译功能包
colcon build --packages-select topic_tutorial

# 加载环境变量(Linux/macOS)
source install/setup.bash
# Windows 用户使用: call install\setup.bat

# 终端 1:先启动 turtlesim
ros2 run turtlesim turtlesim_node

# 终端 2:运行画圆发布者
source install/setup.bash
ros2 run topic_tutorial circle_driver

# 终端 3:运行位姿订阅者
source install/setup.bash
ros2 run topic_tutorial pose_monitor

你应该会看到:终端 2 不断发布速度指令,小海龟在仿真器中做圆周运动,终端 3 实时打印出小海龟的坐标和朝向。恭喜你——你刚刚构建了第一个完整的话题通信系统!


5. 进阶小贴士与避坑指南

5.1 话题重映射(Remapping):不改代码换话题名

在实际项目中,你经常会遇到这种情况:一个节点发布到 /cmd_vel,但你的机器人需要两个独立的速度控制通道。ROS 2 提供了话题重映射功能,允许你在启动节点时动态修改话题名称,无需修改任何代码:

# 将 /cmd_vel 重映射为 /robot1/cmd_vel
ros2 run my_package my_node --ros-args -r /cmd_vel:=/robot1/cmd_vel

这在多机器人部署和系统集成中极为实用,建议尽早掌握。

5.2 跨语言通信:Python 和 C++ 可以无缝对接

ROS 2 的消息接口(.msg 文件)是语言无关的。无论你用 Python、C++ 还是其他支持的语言编写节点,只要话题名称和消息类型一致,就能正常通信。

例如,一个 C++ 编写的发布者向 /cmd_vel 发送 geometry_msgs/msg/Twist,和一个 Python 编写的订阅者监听同一个话题,完全可以互通。这是因为在编译时,ROS 2 的代码生成工具会自动为每种语言生成对应的消息类。

这在实际项目中非常常见:性能敏感的模块(如 SLAM、路径规划)通常用 C++ 编写,而上层逻辑和调试工具则用 Python 快速原型化。

5.3 常见报错排查指南

问题 1:ros2 topic echo 收不到数据

排查清单(按优先级):

  1. 话题名拼写错误:运行 ros2 topic list 确认话题确实存在,注意 / 前缀和大小写。
  2. 发布者在运行吗:运行 ros2 topic info <topic_name> 检查 Publisher count 是否 ≥ 1。
  3. 消息类型不匹配:如果你手动发布消息,确认类型字符串完全正确。
  4. QoS 不兼容:这是最隐蔽的问题。运行 ros2 topic info <topic_name> --verbose 查看双方的 QoS 策略。如果发布者是 Best Effort 而订阅者是 Reliable,连接不会建立。
  5. DDS 域不匹配:检查 ROS_DOMAIN_ID 环境变量——不同域 ID 的节点之间无法通信。

问题 2:colcon build 编译失败

常见原因包括:package.xml 缺少依赖声明、setup.py 的 entry_points 配置错误、Python 模块路径不正确。建议先检查终端输出的完整错误信息,然后逐项核对配置文件。

问题 3:source install/setup.bash 后找不到节点

确认你在工作空间根目录(~/ros2_ws)下执行的 source 命令。如果在错误的目录执行,环境变量的路径会不正确。

5.4 可视化利器:rqt_graph

命令行工具虽然强大,但当系统中节点和话题越来越多时,纯文本输出就变得难以理解。这时 rqt_graph 就是你的救星。

# 启动 rqt_graph(需要 GUI 环境)
ros2 run rqt_graph rqt_graph

rqt_graph 会以图形化方式展示当前系统中所有节点(椭圆/矩形)和话题(箭头),让你一目了然地看到数据流向。你可以通过顶部的下拉菜单选择显示所有节点或只显示有效连接,也可以将图形导出为 PNG 文件,方便写入文档。

实用技巧:在 rqt_graph 的下拉菜单中选择 "Nodes/Topics (all)" 可以显示系统级话题(如 /rosout、/parameter_events),否则默认只显示用户定义的话题。

5.5 进阶阅读推荐

如果你已经掌握了本文的内容,以下方向值得继续深入:

  • Launch 文件:学会用 ros2 launch 一次性启动多个节点——在掌握了单节点的话题通信后,用 Launch 文件统一管理多个节点是自然的下一步。
  • 服务(Service):话题的"兄弟"通信模式,用于请求-响应式的双向通信,例如"请帮我计算一条路径"。
  • 动作(Action):用于长时间运行的任务,支持反馈和取消,例如"导航到目标点"。
  • 自定义消息类型:学习如何用 .msg 文件定义自己的数据结构。
  • 生命周期节点(Lifecycle Node):一种更规范的节点管理模式,支持状态机转换。
  • ROS 2 网络配置:理解 DDS 域、多机通信和网络安全。

参考资料

[1] ROS 2 官方文档 — Understanding Topics. Understanding-ROS2-Topics.html · docs.ros.org

[2] ROS 2 官方文档 — Quality of Service Settings. About-Quality-of-Service-Settings.html · docs.ros.org

[3] ROS 2 Design — QoS Policies. qos.html · design.ros2.org

[4] ROS 2 官方文档 — Writing a Simple Publisher and Subscriber (Python). Writing-A-Simple-Py-Publisher-And-Subscriber.html · docs.ros.org

[5] ROS 2 Design — ROS on DDS. ros_on_dds.html · design.ros2.org

[6] ROS 2 官方文档 — About Different Middleware Vendors. About-Different-Middleware-Vendors.html · docs.ros.org

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐