前言
上一篇我们讲解了话题(Topic),它是异步单向的数据广播通道;

但机器人开发中还有一类场景:需要主动发起请求、等待对方返回结果,比如节点B请求苹果位置执行单次坐标更新、节点A返回苹果位置坐标。

针对这种一问一答、同步交互 的需求,ROS2 提供了服务(Service) 通信模型。
本文结合加法器完整案例,拆解服务通信原理、客户端 / 服务器完整编程流程,最后对比话题与服务的核心差异,帮你分清两种通信方式的适用场景。
一、两数之和 服务通信核心:客户端 Client + 服务器 Server
1. 角色定义
服务器 Server
服务的提供方,持续后台运行,监听指定服务名称。一旦收到客户端请求,自动执行回调函数处理数据,计算完成后返回应答结果。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
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接口
客户端 Client
服务的请求方,主动向指定服务发送携带参数的请求,阻塞等待服务器返回结果,拿到应答后程序完成单次交互。
python
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
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):
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接口
服务 Service
同步交互的中间载体,拥有固定名称(本例 add_two_ints);

python
int64 a # 第一个加数
int64 b # 第二个加数
---
int64 sum # 求和结果
服务接口分为两部分:Request(请求数据)、Response(应答数据),收发两端必须使用完全一致的 .srv 接口文件。
通俗类比
服务就像线下柜台办事窗口:
- 服务器 = 窗口工作人员,全天在岗等待来人;
- 客户端 = 办事群众,主动上前提交材料(请求参数 a、b);
- 工作人员处理材料、算出结果(a+b),把回执单(sum)交还给群众;
- 群众拿到回执就完成本次交互,离开窗口;工作人员继续等待下一位群众。
完整数据流(加法案例)
- 服务器提前启动,监听服务 add_two_ints;
- 客户端启动时传入参数 2 3,封装为 Request 发送给服务;
- 服务器触发回调函数,执行 request.a + request.b = 5,封装进 Response;
- 服务器将应答返回客户端;
- 客户端接收结果并打印 2 + 3 = 5,程序结束。
二、服务器端完整流程解析(adderServer)
对应 「创建服务服务器的程序流程」,代码执行顺序:
- 编程接口初始化:rclpy.init(args=args),初始化 ROS2 运行环境;
- 创建节点并初始化:自定义类继承Node,节点命名service_adder_server;
- 创建服务器对象:create_service(接口类型, 服务名, 回调函数),绑定AddTwoInts接口、服务名add_two_ints;
- 通过回调函数处理服务请求:adder_callback(request, response),自动接收客户端传入的 a、b,求和赋值给response.sum;
- 向客户端反馈应答结果:return response,把计算结果回传给发起请求的客户端;
- 销毁节点并关闭接口:程序终止时释放节点、关闭 ROS2 环境。
服务器核心代码解读
python
# 创建服务,绑定接口、服务名、回调
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 # 返回应答
关键特性:服务器必须持续运行,依靠rclpy.spin(node)循环监听请求,不会主动退出。
三、客户端完整流程解析(adderClient)
对应 PPT「创建服务客户端的程序流程」,代码执行顺序:
- 编程接口初始化:rclpy.init(args=args);
- 创建节点并初始化:节点命名service_adder_client;
- 创建客户端对象:create_client(接口类型, 服务名),绑定和服务器完全一致的服务名与接口;
- 循环等待服务器上线:wait_for_service(),避免服务器未启动直接发送请求报错;
- 创建并发送请求数据:读取终端传入的两个数字,封装进Request,异步调用call_async()发送;
- 等待服务器应答数据:循环spin_once,判断future.done(),拿到返回的response.sum;
- 销毁节点并关闭接口:单次交互完成后直接释放资源。
客户端核心代码解读
python
# 1. 创建客户端
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...')
# 2. 封装请求、异步发送
self.request.a = int(sys.argv[1])
self.request.b = int(sys.argv[2])
self.future = self.client.call_async(self.request)
# 3. 阻塞等待应答
while rclpy.ok():
rclpy.spin_once(node)
if node.future.done():
response = node.future.result()
self.get_logger().info(f'结果:{node.request.a}+{node.request.b}={response.sum}')
break
关键特性:客户端是一次性交互,拿到结果后程序直接退出。
四、实操运行命令
第一步:启动服务器(必须先开)
bash
ros2 run learning_service service_adder_server
终端持续等待,无输出,监听服务请求。
第二步:新开终端,启动客户端并传入两个数字
bash
ros2 run learning_service service_adder_client 2 3
客户端终端输出:
bash
Result of add_two_ints: for 2 + 3 = 5
服务器终端同步打印收到的请求:
Incoming request
a: 2 b: 3
五、话题 Topic 与服务 Service 核心区别(重点区分)
| 对比维度 | 话题 Topic | 服务 Service |
|---|---|---|
| 通信模式 | 异步单向广播 | 同步一问一答 |
| 数据流向 | 发布者持续推送,无返回 | 客户端发请求 → 服务器处理 → 返回应答 |
| 运行生命周期 | 发布 / 订阅节点可长期独立运行,互不影响 | 服务器长期运行;客户端单次请求后自动退出 |
| 数据收发关系 | 一对多、多对多(一个话题多个订阅者) | 一对一单次交互,一次请求对应一次应答 |
| 适用场景 | 持续流式数据:相机图像、雷达点云、底盘速度指令、传感器实时读数 | 单次查询 / 单次操作:加法计算、单次传感器读数、机械臂单次动作、开关设备 |
| 阻塞特性 | 非阻塞,发布者定时推送,订阅者被动接收 | 客户端发送请求后会阻塞等待返回结果 |
| 消息 / 接口结构 | 单一消息(只有 data,无请求应答区分) | 双段接口:Request(入参)+ Response(返回结果) |
场景选择口诀
- 持续不断输出数据流 → 用话题(摄像头、雷达、里程计)
- 单次主动查询、需要返回结果 → 用服务(参数读取、设备控制、运算请求)
六、常见踩坑点
- 客户端、服务器服务名、接口文件不匹配,客户端会一直打印waiting again;
- 先启动客户端、后启动服务器:客户端会持续等待,直到服务器上线才会处理请求;
- 客户端忘记传入终端参数sys.argv1,运行直接报索引错误;
- 服务器缺少rclpy.spin(node),无法监听任何请求;
- 自定义srv接口未在package.xml、setup.py中配置,编译后找不到接口文件。
七、完整开发流程闭环
- 自定义服务接口 .srv 文件,配置功能包编译规则;
- 编写服务器代码,创建服务与处理回调;
- 编写客户端代码,实现等待服务、发送请求、接收应答逻辑;
- colcon build 编译,source 刷新环境变量;
- 先启动服务器,再启动客户端携带参数发起请求;
- 搭配 rqt_graph 可视化查看服务的客户端、服务器绑定关系。