ROS2服务与动作通信:请求响应与目标反馈-结果实战
> 基于Jazzy Jalisco LTS,详解ROS2服务通信(请求响应)和动作通信(目标反馈-结果)的原理、开发实践与选型策略。包含同步异步调用、动作取消机制、完整可运行代码。适合需要双向交互的机器人开发场景。
一、服务通信基础
服务(Service)采用请求-响应模式,客户端发送请求,服务端处理后返回响应。与话题的单向推送不同,服务是双向的,适合单次交互场景。
通信流程
服务特性
| 特性 | 说明 |
|---|---|
| 通信模式 | 请求-响应(一对一) |
| 通信方向 | 双向 |
| 调用方式 | 同步或异步 |
| 反馈机制 | 无(只有最终结果) |
| 可取消 | 不支持 |
| 多对多 | 一个服务可有多个客户端,但一次请求只有一个响应 |
| 典型场景 | 参数查询、触发计算、状态切换 |
二、服务端与客户端开发
服务定义
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)采用目标-反馈-结果模式,适合长时间运行的任务。相比服务,动作有两个关键能力:过程反馈和可取消。
与服务的核心区别
| 维度 | 服务 | 动作 |
|---|---|---|
| 交互模式 | 请求→响应 | 目标→反馈→结果 |
| 执行时长 | 短(毫秒级) | 长(秒级到分钟级) |
| 过程反馈 | 无 | 有(周期性反馈) |
| 可取消 | 不支持 | 支持 |
| 异步性 | 可选 | 天然异步 |
通信流程
动作定义
动作定义文件后缀为.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 动作对比
核心差异总览
| 维度 | 话题 | 服务 | 动作 |
|---|---|---|---|
| 通信模式 | 发布-订阅 | 请求-响应 | 目标-反馈-结果 |
| 通信方向 | 单向 | 双向 | 双向异步 |
| 同步性 | 异步 | 同步或异步 | 异步 |
| 过程反馈 | 无 | 无 | 有 |
| 可取消 | 不适用 | 不支持 | 支持 |
| 多对多 | 一对多/多对多 | 多客户端→一个服务端 | 多客户端→一个服务端 |
| 数据特征 | 连续数据流 | 单次交互 | 长时间任务 |
| 典型场景 | 传感器数据、控制指令 | 参数查询、触发操作 | 导航、机械臂运动 |
选型决策流程
八、命令行工具
服务相关命令
# 列出所有活跃服务
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,转载请注明出处。