前言
本系列博文是本人的学习笔记,自用为主,不是教程,学习请移步其他大佬的相关教程。前几篇学习资源来自鱼香ROS大佬的详细教程,适合深入学习,但对本人这样的初学者不算友好,而且涉及python与C++混合编程内容,增加了学习成本,后续笔记将以**@古月居**的ROS2入门21讲为主,侵权即删。
一、服务通信概述
ROS2中的服务通信 是一种同步通信机制 ,与话题通信(发布-订阅模型)不同,它采用客户端/服务器(CS)模型,类似于"你问我答"的交互方式。当客户端需要数据时,发送请求给服务器;服务器处理请求后返回应答,客户端等待应答结果。
服务通信 vs 话题通信
特性 | 服务通信 | 话题通信 |
---|---|---|
模式 | 请求-响应 | 发布-订阅 |
方向 | 双向 | 单向 |
同步性 | 同步 | 异步 |
典型应用 | 执行动作/获取数据 | 持续数据流(如图像、传感器数据) |
为什么需要服务通信?
比如在机器视觉应用中,我们不需要持续订阅目标位置(话题通信),而是在需要时发送请求获取最新位置(服务通信),这样更高效、更合理。
二、服务通信核心概念
1. 客户端/服务器模型
- 服务器:提供服务,等待客户端请求
- 客户端:发送请求,等待服务器响应
- 服务名:客户端和服务器共同识别的服务标识
2. 同步通信
客户端发送请求后会等待服务器响应,如果服务器长时间无响应,客户端可以判断服务器可能宕机或网络问题。这与话题通信(异步)有本质区别。
3. 一对多通信
- 一个服务器可以被多个客户端使用
- 服务器是唯一的,但客户端可以有多个
三、服务接口定义(.srv文件)
服务通信的核心是数据的定义,数据分为请求数据 和响应数据。
1. .srv文件结构
# 请求数据
int32 num1
int32 num2
---
# 响应数据
int32 sum
---
分隔请求和响应- 每行定义一个数据字段,格式为
数据类型 字段名
2. 创建.srv文件
创建服务接口文件的步骤:
- 创建功能包(如果还没有)
- 在功能包中创建
srv
文件夹 - 在
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
说明:
- 定义了两个请求参数
a
和b
,一个响应参数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
(布尔值,表示是否需要位置)- 响应参数:
x
和y
(目标物体的坐标)
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
七、服务通信总结
服务通信流程
- 创建服务接口:定义.srv文件
- 修改功能包配置:添加依赖和接口生成配置
- 实现服务端 :
- 创建节点
- 创建服务对象
- 实现回调函数
- 实现客户端 :
- 创建节点
- 创建客户端对象
- 发送请求并处理响应
- 编译和运行:编译功能包并运行服务端和客户端
服务通信优势
- 同步交互:客户端可以确认服务器响应状态
- 按需获取:只在需要数据时获取,避免持续通信
- 明确接口:通过.srv文件定义清晰的请求和响应结构
八、常见问题
1. 服务无法找到?
- 检查服务名是否正确
- 确保服务端已启动
- 检查ROS_DOMAIN_ID是否一致(多机通信时)
2. 服务请求超时?
- 服务端未启动
- 服务名不匹配
- 服务端未正确注册服务
3. 如何添加更多服务?
- 创建新的.srv文件
- 修改CMakeLists.txt添加新服务
- 实现新的服务端和客户端
九、扩展学习
- 自定义服务接口:尝试创建更复杂的请求/响应结构
- 多服务集成:在一个节点中提供多个服务
- 服务QoS配置:调整服务通信的可靠性、延迟等参数
- ROS1与ROS2通信:使用ros1_bridge实现跨版本通信