2、ROS2核心概念详解:深入理解节点、话题、服务、动作的通信机制

各位同学,欢迎来到第二章。

上一章我们把ROS2装好了,也跑通了小乌龟。但说实话,那只是「知其然」。这一章,我们来「知其所以然」。ROS2的四个核心概念——节点、话题、服务、动作,是搭建任何机器人平台的基石。我见过不少新手,代码能跑,但一问到「为什么用话题不用服务」就卡壳了。嗯,今天我们就彻底把它掰扯清楚。

2.1 节点:机器人的「器官」

节点,说白了就是ROS2里的一个独立进程。你可以把它想象成机器人的一个器官:眼睛是一个节点,负责看路;手臂是一个节点,负责抓取;大脑是一个节点,负责决策。

每个节点只干一件事,并且干好。这是ROS2的设计哲学——单一职责。我刚开始做项目时,喜欢把所有代码塞进一个文件里,结果调试起来简直噩梦。后来我学乖了,一个传感器一个节点,一个算法一个节点。出了问题,直接定位到那个节点,重启它就行,其他部分不受影响。

我的习惯:每个节点启动时,我都会打印一句日志,比如「[CameraNode] 已启动,发布频率30Hz」。这样在运行中一眼就能看出哪个节点没起来。

创建一个节点很简单,Python里这样写:

import rclpy
from rclpy.node import Node

class MyRobotNode(Node):
    def __init__(self):
        super().__init__('my_robot_node')
        self.get_logger().info('节点已启动!')

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

if __name__ == '__main__':
    main()

注意那个 rclpy.spin(node),它让节点一直活着,等待消息或定时器触发。如果你忘了写这行,节点会瞬间跑完然后退出——我曾经犯过这个错,找了半天bug。

2.2 话题:一对多的「广播」

话题是ROS2里最常用的通信方式。它是异步的、一对多的。发布者只管发,订阅者只管收,双方不需要知道对方的存在。

你想想看,这像不像电台广播?电台不知道谁在听,听众也不知道电台在哪。但信息就这么传过去了。

实际项目中,话题最适合传输连续的数据流

  • 摄像头图像(/camera/image_raw)
  • 激光雷达点云(/scan)
  • 里程计数据(/odom)
  • 电机速度指令(/cmd_vel)

我在做AGV小车时,激光雷达节点以10Hz的频率发布扫描数据。导航节点订阅它,做SLAM建图。电机驱动节点也订阅它,做避障。你看,一个话题被多个节点消费,这就是话题的优势。

发布话题的代码示例:

class Talker(Node):
    def __init__(self):
        super().__init__('talker')
        self.publisher_ = self.create_publisher(String, 'chatter', 10)
        timer_period = 0.5  # 0.5秒发一次
        self.timer = self.create_timer(timer_period, self.timer_callback)

    def timer_callback(self):
        msg = String()
        msg.data = 'Hello, ROS2!'
        self.publisher_.publish(msg)
        self.get_logger().info('发布了: "%s"' % msg.data)
注意:话题的队列大小(上面代码中的10)很重要。如果订阅者处理太慢,消息会积压。设得太小会丢消息,设得太大可能爆内存。我一般根据消息频率和处理耗时来估算,比如30Hz的图像,队列设10就够了。

2.3 服务:一问一答的「请求-响应」

话题是广播,服务就是打电话。你打过去,对方接,然后给你一个答复。它是同步的、一对一的

什么时候用服务?当你需要执行一个操作并等待结果时。比如:

  • 请求机器人拍照并返回图片
  • 请求机械臂移动到某个位置,返回是否成功
  • 请求地图服务器保存当前地图

我记得有一次做巡检机器人,需要远程触发「开始录制视频」的功能。如果用话题,你得一直监听一个状态标志,很麻烦。用服务就简单了:客户端发一个请求,服务端开始录制,返回「录制成功」。干净利落。

服务定义文件(.srv)长这样:

# AddTwoInts.srv
int64 a
int64 b
---
int64 sum

服务端代码:

class AddTwoIntsServer(Node):
    def __init__(self):
        super().__init__('add_two_ints_server')
        self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_callback)

    def add_callback(self, request, response):
        response.sum = request.a + request.b
        self.get_logger().info('收到请求: %d + %d = %d' % (request.a, request.b, response.sum))
        return response
避坑指南:服务调用是阻塞的。如果你的服务端处理耗时很长(比如超过10秒),客户端可能会超时。我曾经在机械臂路径规划服务里踩过这个坑——规划复杂路径要好几秒,客户端直接报超时。后来我把耗时操作放到线程里,先返回一个「已接收」状态,再用话题通知结果。

2.4 动作:带反馈的「长期任务」

动作是ROS2里最强大的通信方式。它结合了话题和服务的特点:

  • 像服务一样,有目标请求和最终结果
  • 像话题一样,可以在执行过程中持续反馈进度
  • 还支持取消任务

什么时候用动作?当任务需要较长时间执行,并且你想知道进度时。比如:

  • 导航到某个目标点(过程中持续反馈当前位置)
  • 机械臂执行一个复杂轨迹(反馈当前关节角度)
  • 无人机起飞到指定高度(反馈当前高度)

动作的定义文件(.action)包含三部分:

# NavigateToPose.action
# 目标
geometry_msgs/PoseStamped target_pose
---
# 结果
bool success
string message
---
# 反馈
geometry_msgs/PoseStamped current_pose
float32 distance_remaining

我做过一个送餐机器人,用动作来实现「送餐到3号桌」。客户端发送目标点,服务端(导航节点)开始规划路径并移动。过程中,客户端实时收到反馈:当前在哪、还剩多远。如果客人取消订单,客户端可以随时取消动作,机器人就会原地停下。这种体验,用话题和服务都很难实现得这么优雅。

2.5 如何选择?一张表说清楚

很多同学会问:我到底该用哪个?我整理了一张表,你照着选就行:

通信方式 通信模式 是否同步 是否支持反馈 典型场景
话题 发布/订阅 异步 传感器数据、控制指令
服务 请求/响应 同步 触发操作、查询状态
动作 目标/结果/反馈 异步 导航、机械臂运动

简单粗暴的选型原则:

  • 数据一直在变,多个节点需要 → 话题
  • 偶尔调用一次,需要立即知道结果 → 服务
  • 任务跑好几秒,想看进度,还能中途取消 → 动作

2.6 实际项目中的组合应用

一个真实的机器人项目,往往是三种通信方式混用的。拿我做的巡检机器人举例:

  • 话题:激光雷达以20Hz发布扫描数据;摄像头以15Hz发布图像;电机驱动订阅/cmd_vel话题接收速度指令
  • 服务:远程控制台调用/start_recording服务开始录像;调用/get_battery服务查询电量
  • 动作:调度系统发送/navigate_to目标点,导航节点执行路径规划并实时反馈位置

你看,各司其职,互不干扰。这就是ROS2的设计魅力。

核心要点回顾:

  • 节点是基本单元,一个节点只做一件事
  • 话题用于连续数据流,一对多异步通信
  • 服务用于请求-响应,同步调用
  • 动作用于长期任务,带进度反馈和取消功能
  • 实际项目中三者配合使用

好了,这一章的内容就到这里。下一章我们会动手写一个完整的节点,把话题、服务、动作都用上。到时候你会发现,理解了这些概念,写代码就像搭积木一样简单。