【ROS2学习笔记】服务

前言

本系列博文是本人的学习笔记,自用为主,不是教程,学习请移步其他大佬的相关教程。前几篇学习资源来自鱼香ROS大佬的详细教程,适合深入学习,但对本人这样的初学者不算友好,而且涉及python与C++混合编程内容,增加了学习成本,后续笔记将以**@古月居**的ROS2入门21讲为主,侵权即删。

一、服务通信概述

ROS2中的服务通信 是一种同步通信机制 ,与话题通信(发布-订阅模型)不同,它采用客户端/服务器(CS)模型,类似于"你问我答"的交互方式。当客户端需要数据时,发送请求给服务器;服务器处理请求后返回应答,客户端等待应答结果。

服务通信 vs 话题通信

特性 服务通信 话题通信
模式 请求-响应 发布-订阅
方向 双向 单向
同步性 同步 异步
典型应用 执行动作/获取数据 持续数据流(如图像、传感器数据)

为什么需要服务通信?

比如在机器视觉应用中,我们不需要持续订阅目标位置(话题通信),而是在需要时发送请求获取最新位置(服务通信),这样更高效、更合理。


二、服务通信核心概念

1. 客户端/服务器模型

  • 服务器:提供服务,等待客户端请求
  • 客户端:发送请求,等待服务器响应
  • 服务名:客户端和服务器共同识别的服务标识

2. 同步通信

客户端发送请求后会等待服务器响应,如果服务器长时间无响应,客户端可以判断服务器可能宕机或网络问题。这与话题通信(异步)有本质区别。

3. 一对多通信

  • 一个服务器可以被多个客户端使用
  • 服务器是唯一的,但客户端可以有多个

三、服务接口定义(.srv文件)

服务通信的核心是数据的定义,数据分为请求数据响应数据

1. .srv文件结构

复制代码
# 请求数据
int32 num1
int32 num2
---
# 响应数据
int32 sum
  • --- 分隔请求和响应
  • 每行定义一个数据字段,格式为数据类型 字段名

2. 创建.srv文件

创建服务接口文件的步骤:

  1. 创建功能包(如果还没有)
  2. 在功能包中创建srv文件夹
  3. srv文件夹中创建.srv文件

四、实战案例:加法求解器

1. 创建工作空间和功能包

复制代码
# 创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src

# 创建功能包(使用Python)
ros2 pkg create --build-type ament_python learning_service --dependencies rclpy

说明

  • --build-type ament_python:指定使用Python构建系统
  • --dependencies rclpy:添加依赖rclpy(ROS2 Python客户端库)
  • learning_service:功能包名称

2. 创建服务接口文件

复制代码
# 创建srv文件夹
mkdir -p ~/ros2_ws/src/learning_service/srv

# 创建AddTwoInts.srv文件
touch ~/ros2_ws/src/learning_service/srv/AddTwoInts.srv

# 编辑AddTwoInts.srv文件
echo -e "int32 a\nint32 b\n---\nint32 sum" > ~/ros2_ws/src/learning_service/srv/AddTwoInts.srv

说明

  • 定义了两个请求参数ab,一个响应参数sum
  • 这是ROS2官方示例服务接口

3. 修改功能包配置文件

编辑package.xml,确保添加了rosidl_default_generators依赖:

复制代码
<build_depend>rosidl_default_generators</build_depend>

编辑CMakeLists.txt,添加服务接口生成配置:

cpp 复制代码
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddTwoInts.srv"
)

说明

  • rosidl_generate_interfaces:指定要生成接口代码的服务文件
  • 这行代码告诉ROS2在编译时生成服务接口的Python/C++代码

4. 编译功能包

复制代码
cd ~/ros2_ws
colcon build --packages-select learning_service

说明

  • colcon build:编译工作空间
  • --packages-select learning_service:只编译learning_service功能包

5. 服务端实现

创建服务端文件:~/ros2_ws/src/learning_service/learning_service/service_adder_server.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供加法器的服务器处理功能
"""

import rclpy                                     # ROS2 Python接口库
from rclpy.node   import Node                    # ROS2 节点类
from learning_interface.srv import AddTwoInts    # 自定义的服务接口

class adderServer(Node):
    def __init__(self, name):
        super().__init__(name)                      # ROS2节点父类初始化
        # 创建服务器对象(接口类型、服务名、服务器回调函数)
        self.srv = self.create_service(
            AddTwoInts, 
            'add_two_ints', 
            self.adder_callback
        )
    
    def adder_callback(self, request, response):
        """服务回调函数,执行收到请求后对数据的处理"""
        response.sum = request.a + request.b        # 完成加法求和计算
        self.get_logger().info('Incoming request\na: %d b: %d' % (request.a, request.b))
        return response                             # 返回计算结果

def main(args=None):
    """ROS2节点主入口main函数"""
    rclpy.init(args=args)                           # ROS2 Python接口初始化
    node = adderServer("service_adder_server")      # 创建ROS2节点对象并进行初始化
    rclpy.spin(node)                                # 循环等待ROS2退出
    node.destroy_node()                             # 销毁节点对象
    rclpy.shutdown()                                # 关闭ROS2 Python接口

if __name__ == '__main__':
    main()

6. 客户端实现

创建客户端文件:~/ros2_ws/src/learning_service/learning_service/service_adder_client.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-发送两个加数,请求加法器计算
"""

import sys
import rclpy                                  # ROS2 Python接口库
from rclpy.node   import Node                 # ROS2 节点类
from learning_interface.srv import AddTwoInts # 自定义的服务接口

class adderClient(Node):
    def __init__(self, name):
        super().__init__(name)                   # ROS2节点父类初始化
        # 创建服务客户端对象(服务接口类型,服务名)
        self.client = self.create_client(
            AddTwoInts, 
            'add_two_ints'
        )
        # 循环等待服务器端成功启动
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        # 创建服务请求的数据对象
        self.request = AddTwoInts.Request()
    
    def send_request(self):
        """发送服务请求的函数"""
        self.request.a = int(sys.argv[1])  # 从命令行参数获取第一个整数
        self.request.b = int(sys.argv[2])  # 从命令行参数获取第二个整数
        # 异步方式发送服务请求
        self.future = self.client.call_async(self.request)
    
def main(args=None):
    """ROS2节点主入口main函数"""
    rclpy.init(args=args)                       # ROS2 Python接口初始化
    node = adderClient("service_adder_client")  # 创建ROS2节点对象并进行初始化
    node.send_request()                         # 发送服务请求
    
    # 循环执行节点,等待服务响应
    while rclpy.ok():
        rclpy.spin_once(node)                   # 执行一次节点回调
        # 检查数据是否处理完成
        if node.future.done():
            try:
                response = node.future.result()  # 获取服务器端的反馈数据
            except Exception as e:
                node.get_logger().info('Service call failed %r' % (e,))
            else:
                # 打印收到的反馈信息
                node.get_logger().info(
                    'Result of add_two_ints: for %d + %d = %d' % (
                        node.request.a, 
                        node.request.b, 
                        response.sum
                    )
                )
            break  # 退出循环
    node.destroy_node()                         # 销毁节点对象
    rclpy.shutdown()                            # 关闭ROS2 Python接口

if __name__ == '__main__':
    main()

7. 修改setup.py文件

编辑~/ros2_ws/src/learning_service/setup.py,添加入口点:

python 复制代码
from setuptools import setup

package_name = 'learning_service'

setup(
    name=package_name,
    version='0.0.0',
    packages=[package_name],
    data_files=[
        ('share/ament_index/resource_index/packages',
            ['resource/' + package_name]),
        ('share/' + package_name, ['package.xml']),
    ],
    install_requires=['setuptools'],
    zip_safe=True,
    maintainer='your_name',
    maintainer_email='your_email@example.com',
    description='TODO: Package description',
    license='TODO: License declaration',
    tests_require=['pytest'],
    entry_points={
        'console_scripts': [
            'service_adder_client = learning_service.service_adder_client:main',
            'service_adder_server = learning_service.service_adder_server:main',
        ],
    },
)

8. 重新编译功能包

复制代码
cd ~/ros2_ws
colcon build --packages-select learning_service

9. 运行服务端和客户端

复制代码
# 服务端(在第一个终端)
source install/setup.bash
ros2 run learning_service service_adder_server

# 客户端(在第二个终端)
source install/setup.bash
ros2 run learning_service service_adder_client 2 3

输出结果

复制代码
[INFO] [service_adder_server]: Incoming request
a: 2 b: 3
[INFO] [service_adder_client]: Result of add_two_ints: for 2 + 3 = 5

五、实战案例:目标物体识别

1. 创建服务接口

learning_service/srv目录下创建GetObjectPosition.srv

复制代码
echo -e "bool get\n---\nint32 x\nint32 y" > ~/ros2_ws/src/learning_service/srv/GetObjectPosition.srv

说明

  • 请求参数:get(布尔值,表示是否需要位置)
  • 响应参数:xy(目标物体的坐标)

2. 服务端实现

创建~/ros2_ws/src/learning_service/learning_service/service_object_server.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-提供目标识别服务
"""

import rclpy                           # ROS2 Python接口库
from rclpy.node import Node            # ROS2 节点类
from sensor_msgs.msg import Image      # 图像消息类型
import numpy as np                     # Python数值计算库
from cv_bridge import CvBridge         # ROS与OpenCV图像转换类
import cv2                             # Opencv图像处理库
from learning_interface.srv import GetObjectPosition   # 自定义的服务接口

# 红色的HSV阈值
lower_red = np.array([0, 90, 128])     # 红色的HSV阈值下限
upper_red = np.array([180, 255, 255])  # 红色的HSV阈值上限

class ImageSubscriber(Node):
    def __init__(self, name):
        super().__init__(name)                              # ROS2节点父类初始化
        # 创建图像订阅者(消息类型、话题名、回调函数、队列长度)
        self.sub = self.create_subscription(
            Image, 
            'image_raw', 
            self.listener_callback, 
            10
        )
        self.cv_bridge = CvBridge()                         # 创建图像转换对象
        # 创建服务服务器(接口类型、服务名、回调函数)
        self.srv = self.create_service(
            GetObjectPosition, 
            'get_target_position', 
            self.object_position_callback
        )
        self.objectX = 0
        self.objectY = 0
    
    def object_detect(self, image):
        """目标检测函数,识别红色物体并计算中心坐标"""
        hsv_img = cv2.cvtColor(image, cv2.COLOR_BGR2HSV)      # 图像从BGR转HSV
        mask_red = cv2.inRange(hsv_img, lower_red, upper_red) # 图像二值化
        # 轮廓检测
        contours, hierarchy = cv2.findContours(
            mask_red, 
            cv2.RETR_LIST, 
            cv2.CHAIN_APPROX_NONE
        )
        # 遍历所有轮廓
        for cnt in contours:
            # 过滤小轮廓(噪声)
            if cnt.shape[0] < 150:
                continue
            # 获取轮廓的边界框
            (x, y, w, h) = cv2.boundingRect(cnt)
            # 画出轮廓
            cv2.drawContours(image, [cnt], -1, (0, 255, 0), 2)
            # 画出中心点
            cv2.circle(image, (int(x+w/2), int(y+h/2)), 5, (0, 255, 0), -1)
            # 更新目标坐标
            self.objectX = int(x+w/2)
            self.objectY = int(y+h/2)
        # 显示处理后的图像
        cv2.imshow("object", image)
        cv2.waitKey(50)
    
    def listener_callback(self, data):
        """图像订阅回调函数"""
        self.get_logger().info('Receiving video frame')  # 日志提示
        # 将ROS图像消息转换为OpenCV图像
        image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8')
        # 执行目标检测
        self.object_detect(image)
    
    def object_position_callback(self, request, response):
        """服务回调函数,处理目标位置请求"""
        if request.get == True:  # 如果请求需要位置
            response.x = self.objectX  # 设置x坐标
            response.y = self.objectY  # 设置y坐标
            self.get_logger().info('Object position\nx: %d y: %d' % (response.x, response.y))
        else:  # 无效请求
            response.x = 0
            response.y = 0
            self.get_logger().info('Invalid command')
        return response  # 返回响应

def main(args=None):
    """ROS2节点主入口main函数"""
    rclpy.init(args=args)  # 初始化ROS2
    node = ImageSubscriber("service_object_server")  # 创建节点
    rclpy.spin(node)  # 等待ROS2退出
    node.destroy_node()  # 销毁节点
    rclpy.shutdown()  # 关闭ROS2

if __name__ == '__main__':
    main()

3. 客户端实现

创建~/ros2_ws/src/learning_service/learning_service/service_object_client.py

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
@作者: 古月居(www.guyuehome.com)
@说明: ROS2服务示例-请求目标识别,等待目标位置应答
"""

import rclpy                            # ROS2 Python接口库
from rclpy.node   import Node           # ROS2 节点类
from learning_interface.srv import GetObjectPosition  # 自定义的服务接口

class objectClient(Node):
    def __init__(self, name):
        super().__init__(name)                  # ROS2节点父类初始化
        # 创建服务客户端(接口类型、服务名)
        self.client = self.create_client(
            GetObjectPosition, 
            'get_target_position'
        )
        # 等待服务可用
        while not self.client.wait_for_service(timeout_sec=1.0):
            self.get_logger().info('service not available, waiting again...')
        # 创建服务请求对象
        self.request = GetObjectPosition.Request()
    
    def send_request(self):
        """发送服务请求的函数"""
        self.request.get = True  # 请求获取目标位置
        # 异步发送服务请求
        self.future = self.client.call_async(self.request)
    
def main(args=None):
    """ROS2节点主入口main函数"""
    rclpy.init(args=args)  # 初始化ROS2
    node = objectClient("service_object_client")  # 创建节点
    node.send_request()  # 发送服务请求
    
    # 等待服务响应
    while rclpy.ok():
        rclpy.spin_once(node)  # 执行一次节点回调
        # 检查请求是否完成
        if node.future.done():
            try:
                response = node.future.result()  # 获取响应
            except Exception as e:
                node.get_logger().info('Service call failed %r' % (e,))
            else:
                # 打印目标位置
                node.get_logger().info(
                    'Result of object position:\n x: %d y: %d' % (
                        response.x, 
                        response.y
                    )
                )
            break  # 退出循环
    node.destroy_node()  # 销毁节点
    rclpy.shutdown()  # 关闭ROS2

if __name__ == '__main__':
    main()

4. 修改setup.py文件

setup.py中添加客户端的入口点:

python 复制代码
entry_points={
    'console_scripts': [
        'service_adder_client = learning_service.service_adder_client:main',
        'service_adder_server = learning_service.service_adder_server:main',
        'service_object_client = learning_service.service_object_client:main',
        'service_object_server = learning_service.service_object_server:main',
    ],
},

5. 重新编译功能包

复制代码
cd ~/ros2_ws
colcon build --packages-select learning_service

6. 运行全流程

复制代码
# 终端1:启动相机节点(需要安装usb_cam包)
source install/setup.bash
ros2 run usb_cam usb_cam_node_exe

# 终端2:启动服务端(目标识别服务)
source install/setup.bash
ros2 run learning_service service_object_server

# 终端3:启动客户端(请求目标位置)
source install/setup.bash
ros2 run learning_service service_object_client

注意 :需要确保已安装usb_cam包,用于获取摄像头图像。如果未安装,可以使用sudo apt install ros-<distro>-usb-cam安装。


六、服务命令行操作

ROS2提供了命令行工具来查看和测试服务:

复制代码
# 查看所有可用服务
ros2 service list

# 查看特定服务的类型
ros2 service type <service_name>

# 调用服务(示例:调用加法服务)
ros2 service call /add_two_ints learning_interface/srv/AddTwoInts "{a: 5, b: 3}"

# 调用目标位置服务
ros2 service call /get_target_position learning_interface/srv/GetObjectPosition "{get: true}"

说明

  • learning_interface/srv/AddTwoInts:服务类型
  • "{a: 5, b: 3}":请求数据,格式为JSON

七、服务通信总结

服务通信流程

  1. 创建服务接口:定义.srv文件
  2. 修改功能包配置:添加依赖和接口生成配置
  3. 实现服务端
    • 创建节点
    • 创建服务对象
    • 实现回调函数
  4. 实现客户端
    • 创建节点
    • 创建客户端对象
    • 发送请求并处理响应
  5. 编译和运行:编译功能包并运行服务端和客户端

服务通信优势

  • 同步交互:客户端可以确认服务器响应状态
  • 按需获取:只在需要数据时获取,避免持续通信
  • 明确接口:通过.srv文件定义清晰的请求和响应结构

八、常见问题

1. 服务无法找到?

  • 检查服务名是否正确
  • 确保服务端已启动
  • 检查ROS_DOMAIN_ID是否一致(多机通信时)

2. 服务请求超时?

  • 服务端未启动
  • 服务名不匹配
  • 服务端未正确注册服务

3. 如何添加更多服务?

  • 创建新的.srv文件
  • 修改CMakeLists.txt添加新服务
  • 实现新的服务端和客户端

九、扩展学习

  1. 自定义服务接口:尝试创建更复杂的请求/响应结构
  2. 多服务集成:在一个节点中提供多个服务
  3. 服务QoS配置:调整服务通信的可靠性、延迟等参数
  4. ROS1与ROS2通信:使用ros1_bridge实现跨版本通信
相关推荐
能不能别报错2 小时前
K8s学习笔记(十一) service
笔记·学习·kubernetes
落羽的落羽2 小时前
【Linux系统】快速入门一些常用的基础指令
linux·服务器·人工智能·学习·机器学习·aigc
Ivanqhz2 小时前
Rust的错误处理
开发语言·后端·rust
easyboot2 小时前
python的print加入颜色显示
开发语言·python
say_fall3 小时前
精通C语言(1.内存函数)
c语言·开发语言
草莓熊Lotso3 小时前
《吃透 C++ vector:从基础使用到核心接口实战指南》
开发语言·c++·算法
东方芷兰4 小时前
LLM 笔记 —— 01 大型语言模型修炼史(Self-supervised Learning、Supervised Learning、RLHF)
人工智能·笔记·神经网络·语言模型·自然语言处理·transformer
-雷阵雨-4 小时前
数据结构——LinkedList和链表
java·开发语言·数据结构·链表·intellij-idea
GHL2842710905 小时前
i++汇编学习
汇编·学习