服务
话题实现了节点之间的通信,但是发布者一旦启动,它就会按照定时器的周期源源不断地给订阅者传数据,订阅者只能被动地接收所有数据——然而,很多时候我们不是这样的工作方式,有时我们希望:
订阅者在需要数据的时候,才去请求发布者的数据,而发布者接收到请求后,才去返回数据。 发布者相当于服务器端,而订阅者相当于客户端
- 客户端/服务器(C/S) 模型
- 服务器端唯一,客户端可以不唯一
- .srv文件定义请求和应答数据结构
我们来实现一个加法求和器,客户端发送加数给服务器端,服务器端相加后再返回结果给客户端。
定义通信接口
通信接口简介(简要步骤与注意事项,针对 C++ 功能包 learning_interface):
接口定义文件
话题:放在
msg/下,文件后缀.msg。服务:放在
srv/下,文件后缀.srv。例如srv/AddTwoInts.srv:srvint64 a int64 b --- int64 sum动作:放在
action/下,文件后缀.action。
在功能包中添加服务的步骤
创建
.srv文件:在learning_interface/srv/AddTwoInts.srv中按上例编写请求与应答字段。更新
package.xml:添加接口生成与运行时的依赖,例如:xml<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <build_depend>rclcpp</build_depend> <exec_depend>rclcpp</exec_depend>rosidl_default_generators用于在编译时生成语言绑定(包含 C++ 头文件)。rosidl_default_runtime在运行时提供接口支持。rclcpp是 C++ 节点的运行时依赖。
更新
CMakeLists.txt:确保启用接口生成并安装生成产物(关键示例片段):cmakefind_package(rosidl_default_generators REQUIRED) find_package(rclcpp REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "srv/AddTwoInts.srv" ) ament_package() # 在定义可执行文件时,使用 ament_target_dependencies 将 rclcpp 等链接到目标必须在 CMake 中调用
rosidl_generate_interfaces()(并在合适位置执行ament_package()),否则不会生成 C++ 的头文件和类型,导致包含失败。在 C++ 代码中使用生成的类型:
- 包含头文件:
#include "learning_interface/srv/add_two_ints.hpp" - 引用类型:
using learning_interface::srv::AddTwoInts; - 创建服务/客户端示例:
node->create_service<AddTwoInts>("add_two_ints", callback);/node->create_client<AddTwoInts>("add_two_ints");
- 包含头文件:
构建与验证(Windows 相关提示)
构建:在工作区根运行
colcon build。在 Windows 上构建完成后,运行:
batcall install\setup.bat以便在当前终端加载生成的环境。
验证接口已生成:
bashros2 interface show learning_interface/srv/AddTwoInts如果
ros2 interface show能正确显示字段,说明接口生成成功。
额外注意事项
- 在 Python 示例中可用
from learning_interface.srv import AddTwoInts;但在 C++ 功能包中需要包含生成的头文件(见上)。 - 服务名(如
add_two_ints)在服务端与客户端必须完全一致。 - 如果在 CMake 中忘记调用
rosidl_generate_interfaces(),将无法生成语言绑定;在 Windows 下尤其常见,务必检查CMakeLists.txt。 - 常用调试:先
colcon build,再查看install/include/learning_interface/srv是否存在add_two_ints.hpp。
服务器端
我们创建learning_service功能包,在learning_service/learning_service文件夹里创建service_adder_server.py
import rclpy
from rclpy.node import Node
from learning_interface.srv import AddTwoInts # 自定义的服务器接口,相当于话题的消息类型
class adderServer(Node):
def __init__(self,name):
super().__init__(name)
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):
rclpy.init(args = args)
node = adderServer("service_adder_server")
rclpy.spin(node)
node.destroy_node()
rclpy.shutdown()create_service: 接收服务器接口类型(此处为AddTwoInts),服务名称,回调函数 当服务器接收到请求时,ROS2底层会:
- 创建请求对象,类型为
AddTwoInts.Request - 创建响应对象,类型为
AddTwoInts.Response - 按位置将这两个对象传递给回调函数
回调函数的参数:self,request,response分别代表:指向自身的指针,客户端发来的请求参数,服务器端返回的结果。request和response是ROS自动生成的类,形参顺序不能变,因为这是回调函数的固定签名。但形参名称可以变,比如简写成req,res.
比如: AddTwoInts.srv里的内容:
int64 a
int64 b
---
int64 sumROS2会自动生成两个类:
AddTwoInts.Request- 包含字段a和bAddTwoInts.Response- 包含字段sum
客户端
新建service_adder_client.py:
import sys # 用于读取命令行参数
import rclpy
from rclpy.node import Node
from learning_interface.srv import AddTwoInts
class adderClient(Node):
def __init__(self,name):
super().__init__(name)
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]) # 从终端用命令行传参,argv[0]是文件名,所以索引从1开始
self.future = self.client.call_async(self.request) # 异步方发送请求
def main(args=None):
rclpy.init(args=args)
node = adderClient("service_adder_client")
node.send_request()
while rclpy.ok():
rclpy.spin_once(node)
if node.future.done():
try:
response = node.future.result()
node.get_logger().info("Result: %d + %d = %d" %
(node.request.a, node.request.b, response.sum))
except Exception as e:
node.get_logger().error("Service call failed: %r" % (e,))
break # 收到响应后退出循环
else:
node.get_logger().info("Waiting for response...")
# 可选:添加延迟避免CPU空转
# rclpy.sleep(0.1)
node.destroy_node()
rclpy.shutdown()create_client: 创建客户端,需要两个参数:服务类型(AddTwoInts),服务名称(必须与服务器端一致) wait_for_service: 阻塞等待直到服务可用 AddTwoInts.Request:new了一个空的请求对象 call_async:client的函数,用于发送异步请求,不阻塞,返回一个Future对象,Future是异步编程的核心概念,代表一个尚未完成的操作的结果:
- call_async()立即返回,不等待服务端响应
- self.future 是一个占位,表示“未来会有的结果”
- 初始状态:未完成(Pending)
- 收到响应后:完成,结果可用
graph LR
A[创建Future] -->|等待中| B[检查done]
B -->|未完成| B
B -->|已完成| C[调用result获取值]
C --> D[成功或抛出异常]Response 是怎么获取到的?
流程详解
# 步骤1:发送请求时创建 Future
self.future = self.client.call_async(self.request)
# Future 内部有一个队列,等待服务端响应
# 步骤2:循环中不断处理
rclpy.spin_once(node) # 这个调用会处理网络消息
# 当服务端响应到达时,spin_once 会将数据填入 future
# 步骤3:检查是否完成
if node.future.done(): # 如果响应已到达
response = node.future.result() # 取出响应数据
# result() 返回的就是 AddTwoInts.Response 对象
# 包含 response.sum 字段