ESC
输入关键词搜索文章标题和内容

ROS2服务与动作通信:请求响应与目标反馈-结果实战

本文由 linuxROS 整理发布,首发于 linuxros.cn,转载请注明出处。

ROS2服务与动作通信:请求响应与目标反馈-结果实战

> 基于Jazzy Jalisco LTS,详解ROS2服务通信(请求响应)和动作通信(目标反馈-结果)的原理、开发实践与选型策略。包含同步异步调用、动作取消机制、完整可运行代码。适合需要双向交互的机器人开发场景。

一、服务通信基础

服务(Service)采用请求-响应模式,客户端发送请求,服务端处理后返回响应。与话题的单向推送不同,服务是双向的,适合单次交互场景。

通信流程

flowchart TB A["客户端发送请求"] --> B["服务端接收请求"] B --> C["执行处理逻辑"] C --> D["返回响应结果"] D --> E["客户端收到响应"] style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#FFF8E1,stroke:#F57C00 style D fill:#E8F5E9,stroke:#388E3C style E fill:#E8F5E9,stroke:#388E3C

服务特性

特性 说明
通信模式 请求-响应(一对一)
通信方向 双向
调用方式 同步或异步
反馈机制 无(只有最终结果)
可取消 不支持
多对多 一个服务可有多个客户端,但一次请求只有一个响应
典型场景 参数查询、触发计算、状态切换

二、服务端与客户端开发

服务定义

ROS2的服务定义文件后缀为.srv,包含Request和Response两部分,用---分隔。
以example_interfaces/srv/AddTwoInts为例:

int64 a
int64 b
---
int64 sum

---上方是请求字段(a、b),下方是响应字段(sum)。

Python服务端

# add_two_ints_server.py
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts


class AddTwoIntsServer(Node):
    def __init__(self):
        super().__init__('add_two_ints_server')
        # 创建服务,指定服务名和回调
        self.srv = self.create_service(
            AddTwoInts, 'add_two_ints', self.add_callback
        )
        self.get_logger().info('服务已启动,等待请求...')

    def add_callback(self, request, response):
        # 计算结果
        response.sum = request.a + request.b
        self.get_logger().info(
            f'收到请求: {request.a} + {request.b} = {response.sum}'
        )
        return response


def main():
    rclpy.init()
    node = AddTwoIntsServer()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

Python客户端

# add_two_ints_client.py
import sys
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts


class AddTwoIntsClient(Node):
    def __init__(self):
        super().__init__('add_two_ints_client')
        # 创建客户端
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        # 等待服务可用
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待服务上线...')

    def send_request(self, a, b):
        # 构造请求
        req = AddTwoInts.Request()
        req.a = a
        req.b = b
        # 同步调用
        future = self.cli.call_async(req)
        rclpy.spin_until_future_complete(self, future)
        return future.result()


def main():
    rclpy.init()
    node = AddTwoIntsClient()
    # 从命令行参数获取两个整数
    a = int(sys.argv[1]) if len(sys.argv) > 1 else 2
    b = int(sys.argv[2]) if len(sys.argv) > 2 else 3
    response = node.send_request(a, b)
    node.get_logger().info(f'结果: {a} + {b} = {response.sum}')
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

运行方式:

# 终端1:启动服务端
ros2 run demo_nodes_py add_two_ints_server

# 终端2:启动客户端
python3 add_two_ints_client.py 41 58

三、同步调用 vs 异步调用

ROS2的服务调用有两种方式,核心区别在于是否阻塞当前线程等待响应。

同步调用

call_async + spin_until_future_complete:阻塞当前线程,直到收到响应或超时。

# sync_call.py
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts


class SyncClient(Node):
    def __init__(self):
        super().__init__('sync_client')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待服务上线...')

    def call(self, a, b):
        req = AddTwoInts.Request()
        req.a = a
        req.b = b
        future = self.cli.call_async(req)
        # 阻塞等待结果
        rclpy.spin_until_future_complete(self, future)
        if future.result() is not None:
            self.get_logger().info(f'同步结果: {future.result().sum}')
        else:
            self.get_logger().error('请求失败')


def main():
    rclpy.init()
    node = SyncClient()
    node.call(10, 20)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

异步调用

call_async + add_done_callback:不阻塞,响应到达时触发回调。

# async_call.py
import rclpy
from rclpy.node import Node
from example_interfaces.srv import AddTwoInts


class AsyncClient(Node):
    def __init__(self):
        super().__init__('async_client')
        self.cli = self.create_client(AddTwoInts, 'add_two_ints')
        while not self.cli.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('等待服务上线...')

    def call(self, a, b):
        req = AddTwoInts.Request()
        req.a = a
        req.b = b
        future = self.cli.call_async(req)
        # 注册回调,响应到达时自动触发
        future.add_done_callback(self.response_callback)

    def response_callback(self, future):
        result = future.result()
        if result is not None:
            self.get_logger().info(f'异步结果: {result.sum}')
        else:
            self.get_logger().error('请求失败')


def main():
    rclpy.init()
    node = AsyncClient()
    node.call(10, 20)
    # 需要spin让回调有机会执行
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

选型对比

场景 推荐方式 原因
初始化阶段查询参数 同步 启动流程必须等结果,阻塞无影响
用户触发单次操作 同步 用户等待反馈,逻辑简单
高频周期性查询 异步 不阻塞主循环,避免卡顿
并发多请求 异步 同时发多个请求,回调各自处理
节点已有spin循环 异步 与现有spin共存,避免嵌套spin

> 注意:spin_until_future_complete内部会调用spin,不要在已有spin的回调中再使用同步调用,否则会死锁。

四、动作通信基础

动作(Action)采用目标-反馈-结果模式,适合长时间运行的任务。相比服务,动作有两个关键能力:过程反馈和可取消。

来自 linuxros.cn · linuxROS

与服务的核心区别

维度 服务 动作
交互模式 请求→响应 目标→反馈→结果
执行时长 短(毫秒级) 长(秒级到分钟级)
过程反馈 无 有(周期性反馈)
可取消 不支持 支持
异步性 可选 天然异步

通信流程

flowchart TB A["客户端发送目标"] --> B{"服务端接受?"} B -->|"接受"| C["执行任务"] B -->|"拒绝"| D["返回拒绝响应"] C --> E["周期性发送反馈"] E --> F{"任务完成?"} F -->|"否"| E F -->|"是"| G["返回最终结果"] C --> H{"客户端取消?"} H -->|"是"| I["停止执行"] H -->|"否"| F I --> J["返回取消结果"] style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#F3E5F5,stroke:#7B1FA2 style D fill:#FFEBEE,stroke:#D32F2F style E fill:#F3E5F5,stroke:#7B1FA2 style F fill:#FFF8E1,stroke:#F57C00 style G fill:#E8F5E9,stroke:#388E3C style I fill:#FFEBEE,stroke:#D32F2F style J fill:#FFEBEE,stroke:#D32F2F

动作定义

动作定义文件后缀为.action,包含三部分:目标(Goal)、结果(Result)、反馈(Feedback),用---分隔。
以example_interfaces/action/Fibonacci为例:

# 目标:计算斐波那契数列的前N项
int32 order
---
# 结果:最终的数列
int32[] sequence
---
# 反馈:当前计算进度
int32[] partial_sequence

五、动作服务端实现

# fibonacci_action_server.py
import rclpy
from rclpy.action import ActionServer, CancelResponse, GoalResponse
from rclpy.callback_groups import ReentrantCallbackGroup
from rclpy.node import Node
from example_interfaces.action import Fibonacci


class FibonacciActionServer(Node):
    def __init__(self):
        super().__init__('fibonacci_action_server')
        # 使用ReentrantCallbackGroup支持并发执行
        self._action_server = ActionServer(
            self,
            Fibonacci,
            'fibonacci',
            execute_callback=self.execute_callback,
            goal_callback=self.goal_callback,
            cancel_callback=self.cancel_callback,
            callback_group=ReentrantCallbackGroup(),
        )
        self.get_logger().info('动作服务端已启动')

    def goal_callback(self, goal_request):
        """决定是否接受目标"""
        self.get_logger().info(f'收到目标请求: order={goal_request.order}')
        # 拒绝非法输入
        if goal_request.order < 0:
            self.get_logger().warn('拒绝:order不能为负数')
            return GoalResponse.REJECT
        return GoalResponse.ACCEPT

    def cancel_callback(self, goal_handle):
        """决定是否接受取消"""
        self.get_logger().info('收到取消请求')
        return CancelResponse.ACCEPT

    async def execute_callback(self, goal_handle):
        """执行目标,发送反馈和结果"""
        self.get_logger().info('开始执行...')
        order = goal_handle.request.order
        feedback_msg = Fibonacci.Feedback()

        # 初始化斐波那契数列
        seq = [0, 1]

        for i in range(1, order):
            # 检查是否被取消——必须周期性检查
            if goal_handle.is_cancel_requested:
                goal_handle.canceled()
                self.get_logger().info('目标已取消')
                result = Fibonacci.Result()
                result.sequence = seq
                return result

            # 计算下一项
            next_val = seq[-1] + seq[-2]
            seq.append(next_val)

            # 发布反馈
            feedback_msg.partial_sequence = seq
            goal_handle.publish_feedback(feedback_msg)
            self.get_logger().info(f'反馈: {seq}')

            # 模拟耗时操作
            import time
            time.sleep(0.5)

        # 完成
        goal_handle.succeed()
        result = Fibonacci.Result()
        result.sequence = seq
        self.get_logger().info(f'完成: {seq}')
        return result


def main():
    rclpy.init()
    node = FibonacciActionServer()
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

关键点:

  • goal_callback返回ACCEPT或REJECT,控制是否执行
  • cancel_callback返回ACCEPT或REJECT,控制是否允许取消
  • execute_callback中必须周期性检查is_cancel_requested,否则取消请求无法生效
  • 使用ReentrantCallbackGroup让执行回调不阻塞其他回调

六、动作客户端实现

# fibonacci_action_client.py
import rclpy
from rclpy.action import ActionClient
from rclpy.node import Node
from example_interfaces.action import Fibonacci


class FibonacciActionClient(Node):
    def __init__(self):
        super().__init__('fibonacci_action_client')
        self._action_client = ActionClient(
            self, Fibonacci, 'fibonacci'
        )
        self._goal_handle = None

    def send_goal(self, order):
        """发送目标"""
        self._action_client.wait_for_server()
        goal_msg = Fibonacci.Goal()
        goal_msg.order = order
        self.get_logger().info(f'发送目标: order={order}')

        # 发送目标,注册反馈回调和目标响应回调
        send_goal_future = self._action_client.send_goal_async(
            goal_msg,
            feedback_callback=self.feedback_callback,
        )
        send_goal_future.add_done_callback(self.goal_response_callback)

    def goal_response_callback(self, future):
        """处理目标接受/拒绝"""
        goal_handle = future.result()
        if not goal_handle.accepted:
            self.get_logger().warn('目标被拒绝')
            return
        self._goal_handle = goal_handle
        self.get_logger().info('目标已接受,等待结果...')
        # 注册结果回调
        get_result_future = goal_handle.get_result_async()
        get_result_future.add_done_callback(self.get_result_callback)

    def feedback_callback(self, feedback_msg):
        """接收反馈"""
        self.get_logger().info(f'收到反馈: {feedback_msg.partial_sequence}')

    def get_result_callback(self, future):
        """获取最终结果"""
        result = future.result().result
        status = future.result().status
        # 状态码:4=SUCCEEDED, 5=CANCELED, 6=ABORTED
        status_names = {4: 'SUCCEEDED', 5: 'CANCELED', 6: 'ABORTED'}
        self.get_logger().info(
            f'结果: {result.sequence}, 状态: {status_names.get(status, status)}'
        )

    def cancel_goal(self):
        """取消目标"""
        if self._goal_handle is not None:
            self.get_logger().info('发送取消请求...')
            future = self._goal_handle.cancel_goal_async()
            future.add_done_callback(self.cancel_done_callback)

    def cancel_done_callback(self, future):
        """取消完成回调"""
        cancel_response = future.result()
        if len(cancel_response.goals_canceling) > 0:
            self.get_logger().info('取消成功')
        else:
            self.get_logger().warn('取消失败')


def main(args=None):
    rclpy.init()
    node = FibonacciActionClient()
    node.send_goal(order=8)
    rclpy.spin(node)
    node.destroy_node()
    rclpy.shutdown()


if __name__ == '__main__':
    main()

状态码说明

状态码 常量 含义
4 SUCCEEDED 目标成功完成
5 CANCELED 目标被客户端取消
6 ABORTED 目标被服务端中止

运行方式:

# 终端1:启动动作服务端
python3 fibonacci_action_server.py

# 终端2:启动动作客户端
python3 fibonacci_action_client.py

七、话题 vs 服务 vs 动作对比

核心差异总览

维度 话题 服务 动作
通信模式 发布-订阅 请求-响应 目标-反馈-结果
通信方向 单向 双向 双向异步
同步性 异步 同步或异步 异步
过程反馈 无 无 有
可取消 不适用 不支持 支持
多对多 一对多/多对多 多客户端→一个服务端 多客户端→一个服务端
数据特征 连续数据流 单次交互 长时间任务
典型场景 传感器数据、控制指令 参数查询、触发操作 导航、机械臂运动

选型决策流程

flowchart TB A["选择通信方式"] --> B{"需要双向交互?"} B -->|"否"| C["话题"] B -->|"是"| D{"任务耗时长?"} D -->|"否(毫秒级)"| E["服务"] D -->|"是(秒级以上)"| F{"需要过程反馈?"} F -->|"否"| G["服务(异步调用)"] F -->|"是"| H["动作"] F --> I{"需要可取消?"} I -->|"否"| G I -->|"是"| H style A fill:#E3F2FD,stroke:#1976D2 style B fill:#FFF8E1,stroke:#F57C00 style C fill:#E8F5E9,stroke:#388E3C style D fill:#FFF8E1,stroke:#F57C00 style E fill:#E8F5E9,stroke:#388E3C style F fill:#FFF8E1,stroke:#F57C00 style G fill:#E8F5E9,stroke:#388E3C style H fill:#F3E5F5,stroke:#7B1FA2 style I fill:#FFF8E1,stroke:#F57C00

八、命令行工具

服务相关命令

# 列出所有活跃服务
ros2 service list

# 查看服务类型
ros2 service type /add_two_ints

# 查找使用某类型的服务
ros2 service find example_interfaces/srv/AddTwoInts

# 调用服务
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 10, b: 20}"

动作相关命令

# 列出所有活跃动作
ros2 action list

# 查看动作详细信息(包括类型、目标/结果/反馈字段)
ros2 action info /fibonacci

# 发送目标并接收反馈
ros2 action send_goal /fibonacci example_interfaces/action/Fibonacci "{order: 5}" --feedback

九、常见问题

Q1:服务调用超时怎么办?

服务设计用于短时操作。如果服务端处理耗时较长(超过几秒),客户端会超时。解决方案:改用动作通信,或增加客户端超时时间。动作天然支持长时间任务,且提供反馈。
Q2:动作取消后服务端仍在执行?

这是最常见的坑。cancel_goal_async只是发送取消请求,服务端必须主动检查goal_handle.is_cancel_requested并退出执行循环。如果不在execute_callback中周期性检查,取消请求会被忽略,服务端继续执行直到完成。
Q3:服务端回调阻塞影响其他请求?

服务端回调在spin线程中执行。如果回调内有耗时操作(如网络请求、文件IO),会阻塞该节点所有回调。解决方案:将耗时操作放到独立线程中执行,或使用ReentrantCallbackGroup配合多线程执行器(MultiThreadedExecutor)。

十、总结

服务适合"一问一答"的短时交互,动作适合"下达指令→跟踪进度→获取结果"的长时间任务。选择的关键看两点:任务是否耗时、是否需要过程反馈或取消能力。

速查表:

场景 通信方式
传感器数据发布 话题
控制指令下发 话题
查询当前参数 服务
触发一次性计算 服务
导航到目标点 动作
机械臂运动规划 动作
长时间数据处理 动作

本文首发于linuxros.cn,转载请注明出处。

版权声明

作者linuxROS
协议本作品采用 CC BY-NC-SA 4.0 许可协议:署名-非商业性使用-相同方式共享
关注欢迎关注微信公众号 linuxROS,获取更多机器人 / 嵌入式 / Linux 干货
返回首页