跳到主要内容

在 ROS2 中,节点之间的通信机制主要有三类:话题(Topic)、服务(Service) 和 动作(Action)。其中,服务是一种 同步的请求/响应通信模式。它与话题的“持续发布-订阅”不同,更类似于我们在日常编程中调用函数:一个节点提出请求,另一个节点接收请求并返回结果

本章将介绍 ROS2 中的服务通信机制,并通过 Python 编写服务端与客户端实现整数加法的完整示例。通过本章将学习到 .srv 接口的使用、客户端与服务端的定义与交互流程,并掌握构建、配置、运行服务系统的方法。

ROS2 服务通信机制

服务的应用场景

与话题相比,服务有其特定的使用场景:

1. 一次性任务

当需要执行一次计算、查询或控制指令,而不是持续不断的数据流时,使用服务最合适。
例如:

  • 请求“两个数的和”并立即得到结果。

  • 查询当前机器人的状态(电量、位置等)。

  • 发送一个简单的控制命令(如“启动电机”或“停止传感器”)。

2. 需要确定结果的场合

服务保证请求-响应的对应性:每一个请求都会得到一个明确的响应,这一点与话题的“尽力而为”模式不同。 因此,在需要确保“命令是否执行成功” ,“查询是否得到结果”的场景中,服务是理想选择。

3. 轻量交互
如果交互非常短暂,并且可以一次完成(例如参数设置、单次计算),服务是最简洁的方式。 如果交互涉及长时间的执行过程(如机器人移动到目标点),则更适合用 Action

服务的组成与实现流程

一个服务通信由三部分组成:

角色功能说明
客户端(Client)向服务端发送请求(request),并等待响应(response)。
服务端(Server)接收请求,处理任务,并返回结果。
.srv 文件定义服务请求和响应的数据结构。

.srv 文件结构如下:

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

中间的 --- 用于分隔请求(上部分)与响应(下部分)的数据结构。这就意味着:客户端在调用服务时必须提供两个整数,服务端在处理完成后返回一个整数。

服务是基于 DDS(数据分发服务)实现的同步远程调用机制。下图展示了在两个不同的节点中创建服务通信方式的服务端和客户端的流程:

ROS2 不同节点下服务通信方式的服务端和客户端使用流程

图 1: ROS2 不同节点下服务通信方式的服务端和客户端使用流程

服务与话题

服务和话题都是 ROS2 中节点间通信的基本机制。 不同的是,话题 适合持续发布数据流,服务 适合一次性请求/响应的任务。

1. 服务常用接口函数

函数/方法用途说明
create_service()在节点中创建服务端接口
create_client()在节点中创建客户端接口
wait_for_service()等待服务端上线
call_async()以异步方式发送请求(推荐使用)
AddTwoInts.Request()创建请求对象,设置请求数据
spin_until_future_complete()等待服务端返回响应结果
rclpy.shutdown()关闭 ROS 2 通信系统

提示:
服务调用是请求→响应的一次性交互; 话题通信是持续发布→订阅的异步过程。

2. 服务与话题的特性对比

特性服务(Service)话题(Topic)
通信模式请求–响应(Request/Response)发布–订阅(Publish/Subscribe)
是否同步同步(可异步调用)异步
使用场景一次性任务、指令、查询持续数据流(传感器、控制量)
接口定义文件.srv 文件(定义请求与响应).msg 文件(定义消息结构)
示例请求“两个数的和”、读取机器人电量发布 IMU、雷达、相机数据

结论:当你希望 节点之间频繁、实时地传输数据,请使用 话题; 当你只需要 一次性获取结果或执行命令,请使用 服务

ROS 2 服务相关常用命令

在你自己编写服务端和客户端之前,ROS2 提供了许多命令行工具帮助你熟悉已有的服务。以下是一些实用命令:

  • ros2 service list:查看当前系统中所有活跃的服务。

  • ros2 service type <服务名称>:查看某个服务的类型。

  • ros2 service find <类型>:查找某个类型的所有服务。

  • ros2 interface show <服务类型>:查看服务类型的定义,包括请求和响应部分。

  • ros2 service call <服务名> <类型> <请求参数>:从命令行调用某个服务。

示例:

ros2 service list
ros2 service type /clear
ros2 interface show example_interfaces/srv/AddTwoInts
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 2, b: 3}"

提示:如需查看更多命令及其使用说明,可使用 ros2 service -h 查看帮助信息,或参考 ROS 2 官方文档。

编写服务功能包

创建功能包

使用如下命令创建一个基于 Python 的 ROS 2 功能包,并引入所需依赖:

cd ~/ros2_ws/src
ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces

说明:

  • rclpy:ROS 2 的 Python 客户端库。

  • example_interfaces:ROS 2 官方提供的接口包,内含 AddTwoInts.srv 文件。

编写服务端节点

py_srvcli/py_srvcli 目录下新建 service_member_function.py 文件:

from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node

class MinimalService(Node):
def __init__(self):
super().__init__('minimal_service')
self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

def add_two_ints_callback(self, request, response):
response.sum = request.a + request.b
self.get_logger().info(f"Incoming request: a={request.a}, b={request.b}, sum={response.sum}")
return response

def main():
rclpy.init()
minimal_service = MinimalService()
rclpy.spin(minimal_service)
minimal_service.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

服务端代码详解

from example_interfaces.srv import AddTwoInts

导入服务类型:从官方提供的 example_interfaces 功能包中导入 AddTwoInts 服务接口。

该 .srv 接口定义如下:

int64 a
int64 b
---
int64 sum

表示服务请求包括两个整数 a 和 b,响应为它们的和 sum。

import rclpy
from rclpy.node import Node

导入 ROS2 Python 库, rclpy 是 ROS2 的 Python 客户端库,提供了节点、话题、服务等通信接口。 Node 是 rclpy 中最基本的单元类,代表一个 ROS2 节点。

class MinimalService(Node):

定义一个服务节点类,叫 MinimalService,它继承自 Node 类。每个 ROS2 节点都是一个 Node 类的实例或子类。

def __init__(self):
super().__init__('minimal_service')

构造函数初始化父类 Node,并设置节点名称为 'minimal_service'。

你在运行时可以通过命令查看这个节点的名字。

self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

创建服务接口:AddTwoInts 是服务类型。'add_two_ints' 是服务的名称(在 ROS 网络中用于标识该服务)。self.add_two_ints_callback 是回调函数,服务收到请求时会调用它处理数据。

def add_two_ints_callback(self, request, response):

定义回调函数,该函数在收到客户端的请求后会被自动调用。 参数解释:

request:包含客户端发送过来的请求数据(即两个整数)。

response:一个空的响应对象,需要在这个函数中填写处理结果返回给客户端。

response.sum = request.a + request.b

从请求中读取 a 和 b,计算它们的和,并赋值给响应中的 sum 字段。

self.get_logger().info(f"Incoming request: a={request.a}, b={request.b}, sum={response.sum}")

使用 ROS 2 的日志系统打印收到请求的详情,方便调试与观察服务行为。

return response

将填充好的响应对象返回给客户端。

def main():

主程序入口(标准 Python 写法)。

rclpy.init()

初始化 ROS 2 通信系统(每个 ROS 程序都必须调用一次)。

minimal_service = MinimalService()

创建服务节点实例。

rclpy.spin(minimal_service)

启动节点,持续运行以处理传入请求,相当于进入“事件循环”,等待客户端发起请求。

minimal_service.destroy_node()
rclpy.shutdown()

结束程序前销毁节点,释放资源。关闭 ROS 2 通信系统。

if __name__ == '__main__':
main()

标准 Python 启动入口:确保当前模块作为主程序运行时执行 main() 函数。

客户端节点与详解

客户端代码

py_srvcli/py_srvcli 目录下新建 client_member_function.py 文件:

import sys
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node

class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
while not self.cli.wait_for_service(timeout_sec=1.0):
self.get_logger().info('Waiting for service...')
self.req = AddTwoInts.Request()

def send_request(self, a, b):
self.req.a = a
self.req.b = b
return self.cli.call_async(self.req)

def main():
rclpy.init()
minimal_client = MinimalClientAsync()
a, b = int(sys.argv[1]), int(sys.argv[2])
future = minimal_client.send_request(a, b)
rclpy.spin_until_future_complete(minimal_client, future)
if future.result():
response = future.result()
minimal_client.get_logger().info(f"Result: {a} + {b} = {response.sum}")
minimal_client.destroy_node()
rclpy.shutdown()

if __name__ == '__main__':
main()

客户端代码详解

以下是 client_member_function.py 代码解释:

import sys
from example_interfaces.srv import AddTwoInts
import rclpy
from rclpy.node import Node
  • 导入部分

    • import sys:用于访问命令行参数,方便从命令行获取要发送给服务端的值。
    • from example_interfaces.srv import AddTwoInts:从ROS 2官方接口包中导入 AddTwoInts 服务,该服务定义了请求包含两个整型数据(ab),响应为它们的和(sum)。
    • import rclpy:导入 ROS 2 的 Python 客户端库,提供 ROS 系统的基础功能。
    • from rclpy.node import Node:从 rclpy 中导入 Node 类,所有 ROS 2 节点都需要从这个类继承。
class MinimalClientAsync(Node):
def __init__(self):
super().__init__('minimal_client_async')
  • 定义 MinimalClientAsync 类

    • 这是一个继承自 Node 的类,代表一个服务客户端节点。
    • 构造函数中使用 super().__init__('minimal_client_async') 调用父类的构造函数,将节点命名为 minimal_client_async,便于在ROS系统中唯一标识该节点。
self.cli = self.create_client(AddTwoInts, 'add_two_ints')
  • 创建服务客户端
    • 调用 create_client() 方法以创建客户端接口。
    • 第一个参数 AddTwoInts 指定了服务类型,该类型包含请求和响应的数据结构;第二个参数 'add_two_ints' 指定了服务的名称(必须与服务端注册的名称一致)。
while not self.cli.wait_for_service(timeout_sec=1.0): 

self.get_logger().info('Waiting for service...')
  • 等待服务上线
    • wait_for_service(timeout_sec=1.0) 方法检查客户端能否成功连接到服务端。如果服务端尚未上线,它将等待,且每隔 1 秒超时一次。
    • 在等待期间,通过 get_logger().info() 打印日志信息,通知用户当前状态。
self.req = AddTwoInts.Request()
  • 创建请求对象
    • 使用 AddTwoInts.Request() 创建一个空的请求对象,该对象会在后续通过客户端发送给服务端。
    • 该请求对象包含两个成员变量 ab,在调用服务之前需要填充具体的整数值。
def send_request(self, a, b):         
self.req.a = a
self.req.b = b
return self.cli.call_async(self.req)
  • 定义发送请求的方法

    • send_request() 方法接收两个参数 ab,分别赋值给请求对象的字段 ab
    • call_async(self.req) 将请求以异步方式发送出去,并返回一个 future 对象,用于后续获取响应结果。
def main():
rclpy.init()
  • main函数:初始化 ROS 2 系统

    • rclpy.init() 是必须的,负责初始化 ROS 2 通信系统,在后续调用节点操作前必不可少。
minimal_client = MinimalClientAsync()
  • 创建客户端节点实例

    • 通过调用 MinimalClientAsync() 构造函数实例化客户端节点。
a, b = int(sys.argv[1]), int(sys.argv[2])
  • 获取命令行参数
    • 使用 sys.argv 从命令行传递的参数中获取两个整数,作为服务请求参数。注意,这里假定用户在命令行中正确传入两个整数。
future = minimal_client.send_request(a, b)
  • 发送异步请求
    • 调用 send_request(a, b) 方法,发出请求,并返回一个 future 对象,该对象表示未来会收到服务端的响应。
rclpy.spin_until_future_complete(minimal_client, future)
  • 等待响应
    • spin_until_future_complete() 将阻塞当前线程,直到 future 对象完成,即服务端返回响应。它同时处理节点的其他回调(如果有),确保事件循环正常运行。
if future.result():
response = future.result()
minimal_client.get_logger().info(f"Result: {a} + {b} = {response.sum}")
  • 处理响应结果
    • future.result() 完成后,获取响应数据。若响应存在,则打印日志信息,显示服务端计算的结果,即两个整数的和。
minimal_client.destroy_node()
rclpy.shutdown()
  • 清理与关闭
    • destroy_node() 显式销毁节点(虽然垃圾回收可以自动完成,但显式销毁有助于资源释放的及时性)。rclpy.shutdown() 关闭 ROS 2 通信系统,结束程序运行。
if __name__ == '__main__':
main()
  • 标准 Python 入口判断
    • 只有当该文件作为主程序执行时,才会调用 main() 函数。这是标准的 Python 模块运行结构。

构建与运行节点

1. 配置 setup.py

编辑 py_srvcli/setup.py,在 entry_points 中添加:

entry_points={
'console_scripts': [
'service = py_srvcli.service_member_function:main',
'client = py_srvcli.client_member_function:main',
],
},

2. 补充 package.xml 信息

编辑 package.xml,确保包含如下内容:

<depend>rclpy</depend>
<depend>example_interfaces</depend>

3. 构建功能包

cd ~/ros2_ws
rosdep install -i --from-path src --rosdistro jazzy -y
colcon build --packages-select py_srvcli
source install/setup.bash

4. 启动服务端

ros2 run py_srvcli service

输出:

[INFO] [minimal_service]: Incoming request: a=3, b=4, sum=7

5. 启动客户端

ros2 run py_srvcli client 3 4

输出:

[INFO] [minimal_client_async]: Result: 3 + 4 = 7

小结与扩展

本章通过构建加法服务系统,学习了 服务的通信模型与使用场景,如何使用 Python 编写服务端与客户端,.srv 类型结构与接口调用方法,如何配置功能包以支持服务运行, 常用的服务相关命令行工具。在 ROS2 中,服务的设计目标是快速返回结果,因为客户端通常会在等待响应。因此,服务不适合用于长时间运行的任务,特别是那些可能需要在特殊情况下被中断或抢占的任务。如果你的服务需要执行一个耗时较长的计算过程,建议使用动作机制来替代。