ROS 2 系列 3 - Service 编程

1. 什么是 Service?和 Topic、Action 有什么不同?

在 ROS 2 中,Service(服务)是基于请求-响应(Request-Reply)模型的同步通信机制。

通信机制方向特点适用场景
Topic单向(发布/订阅)不问结果,只管发,数据流大传感器数据、雷达点云
Service双向(请求/响应)一问一答,瞬间完成,无反馈进度查询参数、开关控制、简单计算
Action双向(目标/反馈/结果)带进度条,可取消,耗时任务导航、机械臂运动、复杂算法

Server(服务端):提供功能的节点,等待他人调用。

Client(客户端):发起请求的节点,发送数据及等待服务器计算结果返回。

注意:Service 的通信是同步逻辑(虽然底层异步,但 API 表现为等待结果)。如果服务器计算耗时过长,客户端将一直阻塞等待,所以 Service 适合短平快的任务。

2. 定义 Service 接口(.srv 文件)

和 Action 一样,Service 也需要自定义数据结构。下面在独立的功能包中定义 .srv 文件。

2.1. 创建接口功能包

cd ~/ros2_ws/src
ros2 pkg create --license Apache-2.0 custom_interfaces --build-type ament_cmake

2.2. 新建 srv 目录,创建接口文件

cd custom_interfaces
mkdir srv
touch srv/AddTwoInts.srv

2.3. 编写 .srv 文件

打开 srv/AddTwoInts.srv,写入以下内容:

int64 a
int64 b
---
int64 sum

语法解释

  • -- 上面是请求(Request)部分,包含两个整数 a 和 b
  • -- 下面是响应(Response)部分,包含一个整数 sum

2.4. 修改 CMakeLists.txt 和 package.xml

2.4.1. 修改 CMakeLists.txt

在 find_package 下面添加:

find_package(rosidl_default_generators REQUIRED)

在文件末尾添加:

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddTwoInts.srv"
)

2.4.2. 修改 package.xml

在 <buildtool_depend> 后面添加:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>

2.5. 编译接口包

回到工作空间根目录,只编译这个接口包:

cd ~/ros2_ws
colcon build --packages-select custom_interfaces
source install/setup.bash

运行 ros2 interface show custom_interfaces/srv/AddTwoInts,如果打印刚写的三行内容,说明接口编译成功。


3. 创建功能包,编写 Service 服务器

创建专门存放 Python 节点的功能包 service_demo_py

3.1. 创建 Python 功能包

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

3.2. 编写服务器节点

进入 service_demo_py/service_demo_py/ 目录,新建 service_server.py 文件:

cd service_demo_py/service_demo_py/
touch service_server.py

写入以下代码:

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
# 导入我们刚刚编译生成的服务接口
from custom_interfaces.srv import AddTwoInts

class AddServiceServer(Node):

    def __init__(self):
        super().__init__('add_service_server')
        # 创建服务:服务类型、服务名称、回调函数
        self.srv = self.create_service(
            AddTwoInts, 
            'add_two_ints', 
            self.add_callback
        )
        self.get_logger().info('Add service is ready. Waiting for requests...')

    def add_callback(self, request, response):
        """
        回调函数固定格式:接收 request,填充 response。
        这个函数执行完,响应就会自动发回给客户端。
        """
        # 执行加法运算
        response.sum = request.a + request.b
        self.get_logger().info(f'Received request: {request.a} + {request.b} = {response.sum}')
        return response  # 必须返回 response

def main(args=None):
    rclpy.init(args=args)
    node = AddServiceServer()
    rclpy.spin(node)  # 保持节点运行,监听请求
    node.destroy_node()
    rclpy.shutdown()

if __name__ == '__main__':
    main()

4. 编写 Service 客户端

在同一目录(service_demo_py/service_demo_py/)下,新建 service_client.py 文件:

touch service_client.py

写入以下代码:

#!/usr/bin/env python3
import rclpy
from rclpy.node import Node
from custom_interfaces.srv import AddTwoInts

class AddServiceClient(Node):

    def __init__(self):
        super().__init__('add_service_client')
        # 创建客户端
        self.client = self.create_client(AddTwoInts, 'add_two_ints')
        # 等待服务器上线(最多等待 1 秒循环检查)
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().warn('Service not available, waiting again...')
        self.req = AddTwoInts.Request()  # 预创建请求对象

    def send_request(self, a, b):
        # 填充请求数据
        self.req.a = a
        self.req.b = b

        # 异步发送请求(不会阻塞,立即返回一个 Future 对象)
        self.future = self.client.call_async(self.req)
        # 当服务器返回响应时,自动调用下面的回调函数
        self.future.add_done_callback(self.response_callback)

        self.get_logger().info(f'Sent request: {a} + {b}')

    def response_callback(self, future):
        # 取出响应结果
        try:
            response = future.result()
            self.get_logger().info(f'Received response: sum = {response.sum}')
        except Exception as e:
            self.get_logger().error(f'Service call failed: {e}')
        # 收到结果后关闭节点(让程序正常退出)
        rclpy.shutdown()

def main(args=None):
    rclpy.init(args=args)
    node = AddServiceClient()
    # 发送请求:计算 3 + 5
    node.send_request(3, 5)
    rclpy.spin(node)  # 保持节点运行,等待回调执行

if __name__ == '__main__':
    main()

5. 配置 setup.py 入口点

为让 ros2 run 能找到这两个节点,需要修改功能包的 setup.py 文件。

打开 service_demo_py/setup.py,在 entry_points 字段中添加以下内容:

entry_points={
    'console_scripts': [
        'service_server = service_demo_py.service_server:main',
        'service_client = service_demo_py.service_client:main',
    ],
},

6. 编译及运行

6.1. 编译工作空间

回到工作空间根目录,编译整个工作空间(接口包和功能包一起编译):

cd ~/ros2_ws
colcon build
source install/setup.bash

6.2. 打开两个终端(都需 source 环境)

终端 1:运行服务端

source ~/ros2_ws/install/setup.bash
ros2 run service_demo_py service_server

终端 2:运行客户端

source ~/ros2_ws/install/setup.bash
ros2 run service_demo_py service_client

7. 用命令行工具调试

7.1. 查看当前系统中所有运行的服务

ros2 service list

7.2. 查看某个服务的接口类型

ros2 service type /add_two_ints

7.3. 查看某个服务的接口详情

ros2 interface show custom_interfaces/srv/AddTwoInts

7.4. 手动调用服务

ros2 service call /add_two_ints custom_interfaces/srv/AddTwoInts "{a: 10, b: 20}"