news 2026/2/17 13:52:11

ROS2核心概念之服务

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ROS2核心概念之服务

通信可以实现多个ROS节点之间数据的单向传输,使用这种异步通信机制,发布者无法准确知道订阅者是否收到消息,本节我们将一起学习ROS另外一种常用的通信方法——服务,可以实现类似你问我答的同步通信效果。

回到顶部

一、 通信模型

在话题章节中,我们通过一个节点驱动相机,发布图像话题,另外一个节点订阅图像话题,并实现对其中橙色物体的识别,此时我们可以按照图像识别的频率,周期得到物体在图片中的位置。

这个位置信息可以继续发给机器人的上层应用使用,比如可以跟随目标运动,或者运动到目标位置附近。然而,有些场景我们并不需要这么高的频率一直订阅物体的位置,而是更希望在需要这个数据的时候,发一个查询的请求,然后尽快得到此时目标的最新位置。

这样的通信模型和话题单向传输有所不同,变成了发送一个请求,反馈一个应答的形式,好像是你问我答一样,这种通信机制在ROS中成为服务,Service。

1.1 客户端/服务器模型

从服务的实现机制上来看,这种你问我答的形式叫做客户端/服务器模型,简称为CS模型,客户端在需要某些数据的时候,针对某个具体的服务,发送请求信息,服务器端收到请求之后,就会进行处理并反馈应答信息。

这种通信机制在生活中也很常见,比如我们经常浏览的各种网页,此时你的电脑浏览器就是客户端,通过域名或者各种操作,向网站服务器发送请求,服务器收到之后返回需要展现的页面数据。

1.2 同步通信

这个过程一般要求越快越好,假设服务器半天没有反应,你的浏览器一直转圈圈,那有可能是服务器宕机了,或者是网络不好,所以相比话题通信,在服务通信中,客户端可以通过接收到的应答信息,判断服务器端的状态,我们也称之为同步通信。

1.3 一对多通信

比如博客园这个网站,服务器是唯一存在的,并没有多个完全一样的博客园网站,但是可以访问博客园网站的客户端是不唯一的,大家每一个人都可以看到同样的界面。所以服务通信模型中,服务器端唯一,但客户端可以不唯一。

1.4 服务接口

和话题通信类似,服务通信的核心还是要传递数据,数据变成了两个部分:

一个请求的数据,比如请求橘子位置的命令;

还有一个反馈的数据,比如反馈橘子坐标位置的数据;

这些数据和话题消息一样,在ROS中也是要标准定义的,话题使用.msg文件定义,服务使用的是.srv文件定义,后续我们会介绍定义的方法。

回到顶部

二、服务案例

接下来,我们就来看看, 服务到底该如何实现。创建my_learning_service的Python版本的功能包;

pi@NanoPC-T6:~/dev_ws$ cd src

pi@NanoPC-T6:~/dev_ws/src$ ros2 pkg create --build-type ament_python my_learning_service

2.1 加法求解器

前面我们对ROS服务通信应该有了基本了解,接下来我们就要开始编写代码啦。还是从一个相对简单的例程开始,也是ROS官方的一个例程,通过服务实现一个加法求解器的功能。

当我们需要计算两个加数的求和结果时,我们应该怎么做呢?我们需要两个实现节点:

客户端节点:将两个加数封装成请求数据,针对服务add_two_ints发送出去;

服务器端节点:收到客户端发送的请求数据后,开始进行加法计算,并将求和结果封装成应答数据反馈给客户端。

2.1.1 客户端

使用VS Code加载功能包my_learning_service,在my_learning_service文件夹下创建service_adder_client.py;

"""

ROS2服务示例-发送两个加数,请求加法器计算

@author: zy

@since : 2025/12/12

"""

import sys

import rclpy # ROS2 Python接口库

from rclpy.node import Node # ROS2 节点类

from my_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):

rclpy.init(args=args) # ROS2 Python接口初始化

node = adderClient("service_adder_client") # 创建ROS2节点对象并进行初始化

node.send_request() # 发送服务请求

while rclpy.ok(): # ROS2系统正常运行

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接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={

'console_scripts': [

'service_adder_client = my_learning_service.service_adder_client:main',

],

},

对以上程序进行分析,如果我们想要实现一个客户端,流程如下:

编程接口初始化;

创建节点并初始化;

创建客户端对象;

创建并发送请求数据;

等待服务器端应答数据;

销毁节点并关闭接口;

服务端代码解析。

2.1.2 服务端

在my_learning_service文件夹下创建service_adder_server.py;

"""

ROS2服务示例-提供加法器的服务器处理功能

@author: zy

@since : 2025/12/12

"""

import rclpy # ROS2 Python接口库

from rclpy.node import Node # ROS2 节点类

from my_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接口

服务器端的实现,有点类似话题通信中的订阅者,并不知道请求数据什么时间出现,也用到了回调函数机制。

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={

'console_scripts': [

'service_adder_client = my_learning_service.service_adder_client:main',

'service_adder_server = my_learning_service.service_adder_server:main',

],

},

对以上程序进行分析,如果我们想要实现一个服务端,流程如下:

编程接口初始化;

创建节点并初始化;

创建服务器端对象;

通过回调函数处进行服务;

向客户端反馈应答结果;

销毁节点并关闭接口。

2.1.3 服务接口定义

有关服务接口AddTwoInts的定义参考《ROS2核心概念之通信接口》,在my_learning_interface功能包下的srv目录下新建AddTwoInts.srv;

int64 a # 第一个加数

int64 b # 第二个加数

---

int64 sum # 求和结果

定义中有两个部分,上边是进行加法运算的两个入参,下边是求和的结果。

完成定义后,还需要在功能包的CMakeLists.txt中配置编译选项,让编译器在编译过程中,根据接口定义,自动生成不同语言的代码:

find_package(rosidl_default_generators REQUIRED)

# 添加要生成的消息/服务/动作

rosidl_generate_interfaces(${PROJECT_NAME}

"srv/AddTwoInts.srv"

)

编译通信接口;

pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_interface

修改功能包my_learning_service依赖package.xml;

<depend>my_learning_interface</depend>

注意此时如果VS Code无法识别到my_learning_service中引入的服务接口,可以尝试重启VS Code试试。

2.1.4 编译运行

编译程序:

pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_service

启动第一个终端,第一个节点是服务端,等待请求数据并提供求和功能;

pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_adder_server

[INFO] [1765696649.228823971] [service_adder_server]: Incoming request

a: 2 b: 3

启动第二个终端,第二个节点是客户端,发送传入的两个加数并等待求和结果;

pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_adder_client 2 3

[INFO] [1765696649.246169398] [service_adder_client]: Result of add_two_ints: for 2 + 3 = 5

2.2 机器视觉识别

好啦,加法求解器已经实现了,回想下刚才我们提到的视觉识别流程,当我们需要知道目标物体位置的时候,通过服务通信的机制,岂不是更加合理。

接着我们采用服务通信的机制对物体位置识别进行改造,此时会有三个节点出现:

相机驱动节点,发布图像数据;

视觉识别节点,订阅图像数据,并且集成了一个服务器端对象,随时准备提供目标位置;

客户端节点,我们可以认为是一个机器人目标跟踪的节点,当需要根据目标运动时,就发送一次请求,然后拿到一个当前的目标位置。

2.2.1 客户端

在my_learning_service文件夹下创建service_object_client.py;

"""

ROS2服务示例-请求目标识别,等待目标位置应答

@author: zy

@since : 2025/12/12

"""

import rclpy # ROS2 Python接口库

from rclpy.node import Node # ROS2 节点类

from my_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):

rclpy.init(args=args) # ROS2 Python接口初始化

node = objectClient("service_object_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 object position:\n x: %d y: %d' %

(response.x, response.y))

break

node.destroy_node() # 销毁节点对象

rclpy.shutdown() # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={

'console_scripts': [

'service_adder_client = my_learning_service.service_adder_client:main',

'service_adder_server = my_learning_service.service_adder_server:main',

'service_object_client = my_learning_service.service_object_client:main',

],

},

2.2.2 服务端

在my_learning_service文件夹下创建service_object_server.py;

"""

ROS2服务示例-提供目标识别服务

@author: zy

@since : 2025/12/12

"""

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 my_learning_interface.srv import GetObjectPosition # 自定义的服务接口

# 橘子的HSV颜色范围

lower_orange = np.array([10, 100, 100]) # 橙色的HSV阈值下限

upper_orange = np.array([25, 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() # 创建一个图像转换对象,用于OpenCV图像与ROS的图像消息的互相转换

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_orange = cv2.inRange(hsv_img, lower_orange, upper_orange) # 图像二值化

contours, hierarchy = cv2.findContours(

mask_orange , cv2.RETR_LIST, cv2.CHAIN_APPROX_NONE) # 图像中轮廓检测

for cnt in contours: # 去除一些轮廓面积太小的噪声

if cnt.shape[0] < 150:

continue

(x, y, w, h) = cv2.boundingRect(cnt) # 得到橘子所在轮廓的左上角xy像素坐标及轮廓范围的宽和高

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) # 使用OpenCV显示处理后的图像效果

cv2.waitKey(50)

def listener_callback(self, data):

self.get_logger().info('Receiving video frame') # 输出日志信息,提示已进入回调函数

image = self.cv_bridge.imgmsg_to_cv2(data, 'bgr8') # 将ROS的图像消息转化成OpenCV图像

self.object_detect(image) # 橘子检测

def object_position_callback(self, request, response): # 创建回调函数,执行收到请求后对数据的处理

if request.get == True:

response.x = self.objectX # 目标物体的XY坐标

response.y = self.objectY

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 Python接口初始化

node = ImageSubscriber("service_object_server") # 创建ROS2节点对象并进行初始化

rclpy.spin(node) # 循环等待ROS2退出

node.destroy_node() # 销毁节点对象

rclpy.shutdown() # 关闭ROS2 Python接口

完成代码的编写后需要设置功能包的编译选项,让系统知道Python程序的入口,打开功能包的setup.py文件,加入如下入口点的配置:

entry_points={

'console_scripts': [

'service_adder_client = my_learning_service.service_adder_client:main',

'service_adder_server = my_learning_service.service_adder_server:main',

'service_object_client = my_learning_service.service_object_client:main',

'service_object_server = my_learning_service.service_object_server:main',

],

},

2.2.3 服务接口定义

有关服务接口GetObjectPosition的定义参考《ROS2核心概念之通信接口》。

2.2.4 编译运行

编译程序:

pi@NanoPC-T6:~/dev_ws$ colcon build --paths src/my_learning_service

启动第一个终端,第一个节点是相机驱动节点,发布图像数据;

pi@NanoPC-T6:~/dev_ws$ ros2 run usb_cam usb_cam_node_exe --ros-args -p video_device:="/dev/video21"

启动第二个终端,第二个是视觉识别节点,订阅图像数据,并且集成了一个服务器端对象,随时准备提供目标位置;

pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_object_server

[INFO] [1765720921.580354648] [service_object_server]: Receiving video frame

[INFO] [1765720921.862313877] [service_object_server]: Receiving video frame

[INFO] [1765720921.929475108] [service_object_server]: Receiving video frame

[INFO] [1765720921.987023129] [service_object_server]: Receiving video frame

[INFO] [1765720922.051179157] [service_object_server]: Receiving video frame

启动第三个终端,第三个节点是客户端,当需要根据目标运动时,就发送一次请求,然后拿到一个当前的目标位置;

pi@NanoPC-T6:~/dev_ws$ ros2 run my_learning_service service_object_client

[INFO] [1765720984.750619129] [service_object_client]: Result of object position:

x: 388 y: 294

2.3 服务命令行操作

查看服务列表:

pi@NanoPC-T6:~/dev_ws$ ros2 service list

/get_target_position

/service_object_server/describe_parameters

/service_object_server/get_parameter_types

/service_object_server/get_parameters

/service_object_server/list_parameters

/service_object_server/set_parameters

/service_object_server/set_parameters_atomically

/set_camera_info

/set_capture

/usb_cam/describe_parameters

/usb_cam/get_parameter_types

/usb_cam/get_parameters

/usb_cam/list_parameters

/usb_cam/set_parameters

/usb_cam/set_parameters_atomically

其中/get_target_position使我们在机器视觉识别中创建的服务。

查看服务数据类型:

pi@NanoPC-T6:~/dev_ws$ ros2 service type /get_target_position

my_learning_interface/srv/GetObjectPosition

发送服务请求:

pi@NanoPC-T6:~/dev_ws$ ros2 service call <service_name> <service_type> <service_data>

通过rqt_graph可以将节点和话题以及他们之间的连接可视化;

pi@NanoPC-T6:~/dev_ws$ rqt_graph

2.4 思考题

话题和服务是ROS中最为常用的两种数据通信方法;

前者适合传感器、控制指令等周期性、单向传输的数据

后者适合一问一答,同步性要求更高的数据,比如获取机器视觉识别到的目标位置。

在机器人开发过程中,类似的通信应用比比皆是,ROS针对绝大部分通用场景,都设计了标准的话题和服务数据类型,比如图像数据、雷达数据、里程计数据等等,不过机器人软硬件繁杂,很多时候这些标准定义也无法满足我们的需求,这个时候,我们就要自定义通信接口了。

参考文章

[1] 古月居ROS2入门教程学习笔记

亲爱的读者和支持者们,自动博客加入了打赏功能,陆陆续续收到了各位老铁的打赏。在此,我想由衷地感谢每一位对我们博客的支持和打赏。你们的慷慨与支持,是我们前行的动力与源泉。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/17 3:56:43

36、函数式输入输出编程指南

函数式输入输出编程指南 1. 文件读取 在编程中,将程序设计为适应文件读取是相对简单的。 FileReader 类与 ConsoleReader 非常相似,唯一的区别在于静态工厂方法必须处理 IOException ,因此它返回的是 Result<Input> 而不是一个普通值。 以下是 FileReader…

作者头像 李华
网站建设 2026/2/17 13:30:15

41、函数式解决常见问题及 XML 读取程序的函数式转换

函数式解决常见问题及 XML 读取程序的函数式转换 在编程过程中,我们经常会遇到各种数据读取和处理的需求,如读取不同格式的属性值、处理 XML 文件等。下面将详细介绍如何函数式地解决这些常见问题,以及如何将一个传统的 XML 读取程序转换为函数式风格。 1. 定义不同数字格…

作者头像 李华
网站建设 2026/2/17 9:18:54

揭秘Apollo技术:壁画修复与保护的智能透视眼

Apollo红外反射成像技术凭借无损检测、高分辨率成像及表层穿透能力&#xff0c;已成为壁画彩绘检测中揭示底层底稿、识别修复痕迹、分析绘画工艺的关键工具&#xff0c;广泛应用于国内外重要壁画保护项目。一、历史创作层的揭示揭示原始线稿&#xff1a;穿透表层颜料&#xff0…

作者头像 李华
网站建设 2026/2/16 12:30:34

基于VUE的社区投诉建议处理与评价系统 [VUE]-计算机毕业设计源码+LW文档

摘要&#xff1a;随着社区规模的不断扩大和居民参与社区事务意识的提高&#xff0c;高效处理社区投诉建议并收集居民评价成为提升社区管理水平的关键。本文设计并实现了基于VUE的社区投诉建议处理与评价系统&#xff0c;旨在优化社区管理流程&#xff0c;增强居民与社区管理部门…

作者头像 李华
网站建设 2026/2/15 15:00:36

Transmission Docker 容器化部署指南

概述 Transmission 是由LinuxServer.io团队提供的容器化应用&#xff0c;基于Transmission BitTorrent客户端构建。Transmission设计理念为简单易用且功能强大&#xff0c;具备BitTorrent客户端所需的核心特性&#xff1a;加密传输、Web管理界面、节点交换、磁力链接支持、DHT…

作者头像 李华
网站建设 2026/2/17 7:15:32

9、Ansible Container 构建与定制 MariaDB 容器指南

Ansible Container 构建与定制 MariaDB 容器指南 1. MariaDB 配置文件与变量 在使用 Ansible Container 构建 MariaDB 容器时,配置文件起着关键作用。以下是一个典型的 MariaDB 配置文件 my.cnf.j2 : # Ansible Container Generated MariaDB Config File [client] port…

作者头像 李华