1. 项目概述:为什么一个简单的加法服务,值得花20分钟认真写透?
在ROS 2的世界里,“写个服务和客户端”听起来像教科书里的Hello World——但实际动手时,90%的新手会在第三步就卡住:不是setup.py里少了个逗号导致ros2 run报错找不到入口,就是客户端死活连不上服务,日志里反复刷着“service not available, waiting again...”,而服务端压根没打印任何启动信息。我带过十几期ROS 2线下工作坊,每次都有人举手问:“老师,我的client和service明明都ros2 run起来了,为什么它们就像两个平行宇宙里的人,永远无法对话?”——问题从来不在逻辑,而在那些文档里轻描淡写、却决定成败的上下文细节。
这篇内容讲的,就是一个最朴素的整数加法服务(AddTwoInts),但它不是代码搬运工式的教程复述。它是我把ROS 2服务通信机制拆开、揉碎、再亲手焊回去的过程实录。你会看到:为什么必须用call_async()而不是call()?为什么wait_for_service()要配timeout_sec=1.0而不是0.1?为什么package.xml里只写example_interfaces就够了,而不用手动声明.srv文件路径?这些答案,藏在ROS 2的节点生命周期、服务发现协议、以及Python异步事件循环的底层咬合里。它适合三类人:刚配好ROS 2环境、想验证“第一个可运行服务”的纯新手;被rclpy.spin_until_future_complete绕晕、急需理清异步调用链的中级实践者;还有那些准备带新人、需要一份能直接当教案用的带注释完整工程的带队工程师。核心关键词是L3 | Tutorials > Beginner: Client libraries > Writing a simple service and client (Python)——这不是入门的终点,而是你真正开始理解ROS 2“请求-响应”范式的第一块真实路标。
2. 整体设计与思路拆解:服务通信不是魔法,是三重契约的精密执行
ROS 2的服务通信,本质是三个层面的契约协同:接口契约、节点契约、构建契约。忽略其中任何一层,你的服务就会变成“看起来在跑,实际上已失联”的幽灵进程。很多教程只讲“怎么写”,却不解释“为什么这样写”,结果就是复制粘贴能跑,改一行就崩。下面我带你一层层剥开这个加法服务的设计内核。
2.1 接口契约:.srv文件是服务世界的宪法,不是可有可无的配置
你可能注意到,整个项目里没有一行代码去定义“请求两个整数、返回一个整数”这个逻辑。它完全来自example_interfaces包里的AddTwoInts.srv文件:
int64 a int64 b --- int64 sum这行代码不是示例,而是强制约定。ROS 2在编译时会解析.srv文件,自动生成Python类(如AddTwoInts.Request和AddTwoInts.Response),并注入到rclpy的序列化/反序列化管道中。这意味着:
- 服务端
response.sum = request.a + request.b能直接赋值,是因为生成的Response类里真有sum这个属性,且类型是int64(对应Python的int); - 客户端
self.req.a = a能成功,是因为Request类里真有a和b这两个字段; - 如果你手写一个
MyAdd.srv,哪怕只是把int64改成int32,ROS 2也会在编译时报错,因为C++后端生成的类型不匹配。
提示:
example_interfaces是ROS 2官方预置的“标准接口库”,类似Linux的libc。它包含AddTwoInts、Trigger、SetBool等常用服务定义。不要自己造轮子去重写AddTwoInts.srv——除非你明确知道要扩展功能(比如加个string comment字段),否则直接复用它,省去接口兼容性验证的麻烦。
2.2 节点契约:服务名(Service Name)是节点间的唯一身份证,大小写和下划线都不能错
服务端调用self.create_service(AddTwoInts, 'add_two_ints', ...),客户端调用self.create_client(AddTwoInts, 'add_two_ints')。这里的'add_two_ints'不是随便起的昵称,而是ROS 2网络中的全局唯一服务标识符(Service Name)。它的规则比话题(Topic)更严格:
- 必须是全小写+下划线(
add_two_ints合法,AddTwoInts或add-two-ints非法); - 不能以数字开头(
1add_two_ints非法); - 在同一ROS 2域(Domain ID)内,不允许存在同名但类型不同的服务(比如一个
AddTwoInts和一个AddThreeInts都叫add_two_ints,ROS 2会拒绝启动第二个)。
我踩过的坑:曾把服务名写成add_two_int(少了个s),客户端日志疯狂刷service not available,而服务端日志安静如鸡——因为服务根本没注册成功,ros2 node list里都看不到它。排查方法很简单:在服务启动后,立刻在另一个终端执行ros2 service list,确认/add_two_ints是否在列表中;再执行ros2 service type /add_two_ints,确认返回的是example_interfaces/srv/AddTwoInts。这两步,是验证“接口契约”和“节点契约”是否生效的黄金检查点。
2.3 构建契约:setup.py的entry_points不是语法糖,是ROS 2发现可执行文件的唯一路径
ros2 run py_srvcli service这条命令能成功,完全依赖setup.py里这行:
'service = py_srvcli.service_member_function:main'它告诉ROS 2:“当你看到ros2 run py_srvcli service时,请去py_srvcli包下的service_member_function.py文件里,找到main函数并执行它”。这个映射关系,在colcon build时被写入install/py_srvcli/share/py_srvcli/package.xml和install/py_srvcli/lib/python3.x/site-packages/py_srvcli-*.egg-info/entry_points.txt中。如果漏掉这行,或者写成'service = py_srvcli.service_member_function:main_func'(函数名错了),ros2 run会直接报错Executable 'service' not found。更隐蔽的错误是:setup.py里packages=find_packages()没包含py_srvcli子目录,导致colcon build后install/py_srvcli/lib/python3.x/site-packages/下空空如也——此时ros2 run甚至不会报错,而是静默失败。所以,每次修改setup.py后,务必执行colcon build --packages-select py_srvcli && source install/setup.bash再测试,这是构建契约的铁律。
3. 核心细节解析与实操要点:从代码行到运行时的每一处关键决策
现在我们把目光聚焦到两份核心代码上。它们看似简单,但每行代码背后都有明确的工程意图。我会逐段拆解,告诉你“为什么这么写”,而不仅是“怎么写”。
3.1 服务端代码深度解析:service_member_function.py
from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Nodefrom example_interfaces.srv import AddTwoInts:导入的是生成的服务类,不是.srv文件本身。ROS 2的ament_cmake_python或setuptools在构建时,会扫描package.xml里的<depend>example_interfaces</depend>,自动触发example_interfaces的编译,并将生成的Python绑定模块安装到Python路径中。import rclpy:这是ROS 2 Python客户端库的主干,所有节点操作(创建、销毁、发布、订阅、服务)都通过它。注意,它不是rospy的升级版,而是全新设计的异步优先API,与ROS 1有根本性差异。
class MinimalService(Node): def __init__(self): super().__init__('minimal_service') self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)super().__init__('minimal_service'):节点名'minimal_service'是调试时的日志前缀,也是ros2 node list里显示的名字。它不影响服务通信,但选一个清晰的名字(比如'addition_server')能让多人协作时快速定位。self.create_service(...):这是服务注册的核心。AddTwoInts是服务类型(确保与客户端一致),'add_two_ints'是服务名(确保与客户端一致),self.add_two_ints_callback是回调函数(处理请求的逻辑)。关键点:create_service是同步阻塞调用,它会等待ROS 2底层完成服务注册(包括向参数服务器注册服务元数据),完成后才返回。这意味着,__init__结束时,服务已准备好接收请求。
def add_two_ints_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 responseresponse.sum = request.a + request.b:这里没有类型转换,因为request.a和request.b是int64,Python的int天然支持。但如果请求里传了浮点数(比如客户端误传2.5),ROS 2会在序列化时抛出TypeError,因为int64字段只接受整数。self.get_logger().info(...):日志是调试服务的命脉。INFO级别足够,DEBUG会刷屏。注意\n换行符在终端日志里会正确显示,让a:和b:分行,比写成'a: %d, b: %d'更易读。return response:必须返回response对象。这是ROS 2的硬性要求,不返回或返回None会导致客户端超时。
def main(): rclpy.init() minimal_service = MinimalService() rclpy.spin(minimal_service) rclpy.shutdown()rclpy.init():初始化ROS 2客户端库,必须在创建任何节点前调用。它会设置信号处理器(捕获Ctrl+C)、初始化内部事件循环等。rclpy.spin(minimal_service):这是服务端的“心脏”。它让节点进入事件循环,持续监听网络上的服务请求、定时器事件、参数变更等。spin()是阻塞调用,程序会一直停在这里,直到节点被显式关闭(Ctrl+C)或rclpy.shutdown()被调用。切记:spin()之后的代码永远不会执行,所以rclpy.shutdown()必须放在try/except或finally块里,否则Ctrl+C后资源无法释放。
3.2 客户端代码深度解析:client_member_function.py
import sys from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Nodeimport sys:用于读取命令行参数(sys.argv[1]和sys.argv[2])。这是客户端最简化的输入方式,生产环境应替换为declare_parameter和get_parameter,支持从launch文件或参数服务器注入。
class MinimalClientAsync(Node): def __init__(self): super().__init__('minimal_client_async') self.cli = self.create_client(AddTwoInts, 'add_two_ints') while not self.cli.wait_for_service(timeout_sec=1.0): self.get_logger().info('service not available, waiting again...') self.req = AddTwoInts.Request()self.create_client(...):创建客户端对象。它不立即连接服务,只是准备好发送请求的能力。真正的连接发生在call_async()时。while not self.cli.wait_for_service(timeout_sec=1.0)::这是客户端健壮性的关键。wait_for_service()会向ROS 2发现层查询是否存在名为'add_two_ints'的服务。timeout_sec=1.0表示每次查询最多等1秒,然后返回False,进入下一次循环。为什么是1.0秒而不是0.1秒?因为ROS 2的服务发现有网络传播延迟,0.1秒太短,可能导致客户端在服务刚启动、尚未完成广播前就放弃;1秒是经验值,在大多数局域网环境下足够稳定。如果服务永远不存在,这个循环会无限打印日志——生产环境应加最大重试次数(如for i in range(10):)。self.req = AddTwoInts.Request():提前创建请求对象,避免在send_request里重复实例化。AddTwoInts.Request()是一个空壳,a和b字段初始为0,后续会被赋值。
def send_request(self, a, b): self.req.a = a self.req.b = b return self.cli.call_async(self.req)return self.cli.call_async(self.req):这是异步非阻塞调用。它立即将请求发出去,然后立刻返回一个Future对象(类似Python的asyncio.Future),不等待响应。这是ROS 2推荐的方式,因为它不会阻塞节点的其他工作(比如同时处理多个请求、响应定时器事件)。Future对象封装了请求的状态(pending、done、cancelled)和最终结果。
def main(): rclpy.init() minimal_client = MinimalClientAsync() future = minimal_client.send_request(int(sys.argv[1]), int(sys.argv[2])) rclpy.spin_until_future_complete(minimal_client, future) response = future.result() minimal_client.get_logger().info('Result of add_two_ints: for %d + %d = %d' % (int(sys.argv[1]), int(sys.argv[2]), response.sum)) minimal_client.destroy_node() rclpy.shutdown()rclpy.spin_until_future_complete(minimal_client, future):这是客户端的“等待响应”逻辑。它让节点进入一个短暂的事件循环,只监听future的状态变化。一旦future变为done(收到响应或超时),循环立即退出。这是ROS 2异步模型的核心:节点不“挂起”,而是“专注等待一个特定事件”。response = future.result():获取future的结果。如果服务正常响应,result()返回AddTwoInts.Response对象;如果超时或服务崩溃,result()会抛出rclpy.executors.TimeoutException或rclpy.executors.RPCException。生产环境必须用try/except捕获这些异常,否则程序会崩溃。minimal_client.destroy_node():显式销毁节点,释放内存和网络句柄。虽然rclpy.shutdown()会清理,但显式调用是良好习惯,尤其在复杂节点中。
4. 实操过程与核心环节实现:从零开始构建可运行服务的完整流水线
现在,我们把所有理论落地为可执行的步骤。以下流程基于Ubuntu 22.04 + ROS 2 Jazzy(Jazzy Jalisco),但核心逻辑适用于Humble、Foxy等所有LTS版本。每一步我都标注了目的、常见错误和验证方法,确保你不是机械执行,而是理解每一步在做什么。
4.1 环境准备与工作空间搭建
目的:创建一个隔离的ROS 2开发环境,避免污染系统级安装。
操作:
# 1. 创建工作空间目录(推荐放在家目录下,路径不含空格和中文) mkdir -p ~/ros2_ws/src # 2. 源码ROS 2安装(假设已按官方指南安装Jazzy) source /opt/ros/jazzy/setup.bash # 3. 进入src目录,创建新包 cd ~/ros2_ws/src ros2 pkg create --build-type ament_python --license Apache-2.0 py_srvcli --dependencies rclpy example_interfaces常见错误与验证:
- 错误:
ros2: command not found→ 未正确sourceROS 2环境。执行echo $ROS_DISTRO,应输出jazzy;执行which ros2,应返回/opt/ros/jazzy/bin/ros2。 - 错误:
Permission denied→ 当前用户不在ros组。执行sudo usermod -aG ros $USER,然后重启终端。 - 验证:
ls -l ~/ros2_ws/src/py_srvcli/应看到package.xml、setup.py、setup.cfg、pyproject.toml等文件;cat ~/ros2_ws/src/py_srvcli/package.xml | grep example_interfaces应输出<depend>example_interfaces</depend>,证明依赖已自动添加。
4.2 配置文件精细化修改
目的:让包符合ROS 2规范,支持colcon build和ros2 run。
操作:
- 编辑
~/ros2_ws/src/py_srvcli/package.xml,在<description>标签内补充描述,添加维护者信息:
<description>Python service and client tutorial for integer addition</description> <maintainer email="your.email@example.com">Your Name</maintainer> <license>Apache-2.0</license>- 编辑
~/ros2_ws/src/py_srvcli/setup.py,在setup()函数的entry_points参数中,添加服务和客户端的入口:
entry_points={ 'console_scripts': [ 'service = py_srvcli.service_member_function:main', 'client = py_srvcli.client_member_function:main', ], },常见错误与验证:
- 错误:
setup.py里entry_points拼写错误(如写成entry_point单数)→colcon build会成功,但ros2 run找不到命令。 - 错误:
package.xml里<maintainer>邮箱格式错误(如缺少@)→colcon build会警告,但通常不影响运行。 - 验证:执行
colcon build --packages-select py_srvcli,观察输出末尾是否有Finished字样;然后执行source install/setup.bash,再执行ros2 run py_srvcli,应列出service和client两个可执行项。
4.3 服务端与客户端代码编写
目的:实现加法逻辑,确保代码结构符合ROS 2 Python最佳实践。
操作:
- 创建服务端文件:
touch ~/ros2_ws/src/py_srvcli/py_srvcli/service_member_function.py # 将教程中的完整代码粘贴进去- 创建客户端文件:
touch ~/ros2_ws/src/py_srvcli/py_srvcli/client_member_function.py # 将教程中的完整代码粘贴进去关键细节补全(教程未提及,但实操必需):
- 在
service_member_function.py顶部添加#!/usr/bin/env python3,并赋予执行权限:chmod +x ~/ros2_ws/src/py_srvcli/py_srvcli/service_member_function.py。虽然ros2 run不依赖此,但某些IDE或调试工具需要。 - 在
client_member_function.py的main()函数中,必须添加异常处理:
try: rclpy.spin_until_future_complete(minimal_client, future) response = future.result() minimal_client.get_logger().info('Result of add_two_ints: for %d + %d = %d' % (int(sys.argv[1]), int(sys.argv[2]), response.sum)) except Exception as e: minimal_client.get_logger().error('Service call failed %r' % (e,)) finally: minimal_client.destroy_node() rclpy.shutdown()验证:执行python3 ~/ros2_ws/src/py_srvcli/py_srvcli/service_member_function.py,应无语法错误;执行python3 ~/ros2_ws/src/py_srvcli/py_srvcli/client_member_function.py 1 2,应报错rclpy.init() has not been called——这证明Python脚本本身语法正确,只是缺少ROS 2初始化。
4.4 构建、启动与通信验证
目的:让服务和客户端真正跑起来,并验证数据流正确。
操作:
# 1. 构建包(在工作空间根目录) cd ~/ros2_ws colcon build --packages-select py_srvcli # 2. 源码安装环境(每次打开新终端都要做) source install/setup.bash # 3. 启动服务端(新开终端) ros2 run py_srvcli service # 4. 启动客户端(再开一个终端,同样先source) ros2 run py_srvcli client 5 7预期输出:
- 服务端终端:
[INFO] [minimal_service]: Incoming request a: 5 b: 7 - 客户端终端:
[INFO] [minimal_client_async]: Result of add_two_ints: for 5 + 7 = 12
深度验证技巧(超越教程):
- 网络层验证:在服务启动后,执行
ros2 node info /minimal_service,查看Services部分是否列出/add_two_ints;执行ros2 interface show example_interfaces/srv/AddTwoInts,确认接口定义正确。 - 压力测试:写一个简单脚本,连续发送100次请求:
for i in {1..100}; do ros2 run py_srvcli client $i $((i+1)); done观察服务端日志是否稳定,无丢包或超时。这能暴露spin()的稳定性。
- 跨机器通信:如果服务和客户端在不同机器上,需设置
ROS_DOMAIN_ID(如export ROS_DOMAIN_ID=42)并确保防火墙开放UDP端口(默认8000-8999)。这是ROS 2多机协作的基础,但教程完全没提。
5. 常见问题与排查技巧实录:那些让新手熬夜到凌晨的“幽灵Bug”
在真实项目中,服务通信失败的原因往往不是代码逻辑错,而是环境、配置或认知偏差导致的“隐形墙”。以下是我在教学和项目中高频遇到的5个典型问题,附带可立即执行的排查命令和修复方案。
5.1 问题:客户端日志狂刷service not available, waiting again...,服务端无任何日志
现象:ros2 run py_srvcli client 1 2后,客户端终端不断打印等待日志,服务端终端一片空白,ros2 node list里也看不到/minimal_service。
根本原因:服务端节点根本没启动成功,create_service调用失败,但错误被静默吞掉。
排查步骤:
- 检查服务端是否真的在运行:执行
ps aux | grep service_member_function,如果无输出,说明进程已退出。 - 手动运行服务端看报错:在服务端终端执行
python3 ~/ros2_ws/src/py_srvcli/py_srvcli/service_member_function.py。常见错误:ModuleNotFoundError: No module named 'example_interfaces'→source install/setup.bash没执行,或colcon build失败。rclpy._rclpy.RCLError: Failed to create service: service name '/add_two_ints' is invalid→ 服务名含非法字符(如大写字母、短横线)。
- 检查ROS 2域ID一致性:执行
echo $ROS_DOMAIN_ID,确保服务端和客户端终端输出相同。默认为0,但若某终端设置了export ROS_DOMAIN_ID=1,它们就无法通信。
修复方案:统一source install/setup.bash,确保ROS_DOMAIN_ID一致,修正服务名。
5.2 问题:客户端收到响应,但response.sum是0,而非预期的和
现象:客户端日志显示Result of add_two_ints: for 2 + 3 = 0,服务端日志显示Incoming request a: 2 b: 3。
根本原因:服务端回调函数中,response.sum = request.a + request.b执行了,但return response被遗漏或写在了条件分支外。
排查步骤:
- 检查服务端代码:确认
add_two_ints_callback函数末尾有return response,且不在if语句内。 - 添加调试日志:在
return response前加一行self.get_logger().info('Calculated sum: %d' % response.sum),确认计算值正确。 - 检查
.srv文件生成:执行ls -l install/example_interfaces/share/example_interfaces/srv/,确认AddTwoInts.srv存在,且install/py_srvcli/lib/python3.x/site-packages/example_interfaces/srv/下有对应的Python文件(如_AddTwoInts_Srv.py)。
修复方案:补全return response,确保.srv文件被正确编译。
5.3 问题:ros2 run报错Executable 'service' not found
现象:source install/setup.bash后,执行ros2 run py_srvcli service,提示Executable 'service' not found。
根本原因:setup.py中的entry_points未被colcon build正确解析,或package.xml中<exec_depend>缺失。
排查步骤:
- 检查
setup.py格式:确认entry_points是字典,键为'console_scripts',值为字符串列表,且逗号后有空格('service = ...',)。Python对缩进和逗号极其敏感。 - 检查
package.xml依赖:执行cat ~/ros2_ws/src/py_srvcli/package.xml | grep exec_depend,应有<exec_depend>rclpy</exec_depend>和<exec_depend>example_interfaces</exec_depend>。exec_depend是运行时依赖,depend是构建时依赖,两者都要有。 - 检查构建产物:执行
ls install/py_srvcli/lib/python3.x/site-packages/py_srvcli-*.egg-info/entry_points.txt,文件应存在,且内容包含service = py_srvcli.service_member_function:main。
修复方案:修正setup.py语法,补全package.xml中的<exec_depend>,重新colcon build。
5.4 问题:服务端Ctrl+C后,客户端仍报service not available
现象:停止服务端后,客户端重启,日志仍显示waiting again...,即使等待几分钟也不恢复。
根本原因:ROS 2的服务发现有缓存机制,客户端可能还记着旧的服务地址。
排查步骤:
- 清除ROS 2参数缓存:执行
ros2 param set /minimal_client_async use_sim_time false(任意参数),强制刷新节点状态。 - 重启DDS中间件:执行
killall -9 rmw_implementation(针对Fast DDS)或killall -9 cyclonedds(针对Cyclone DDS),然后重启服务端和客户端。 - 检查网络连接:执行
ping <服务端IP>,确认网络通畅。
修复方案:最可靠的是重启客户端节点,或在客户端代码中加入self.cli.destroy()后重建客户端的逻辑。
5.5 问题:rclpy.spin_until_future_complete在回调中使用,导致死锁
现象:将rclpy.spin_until_future_complete写在服务端的add_two_ints_callback里,程序卡死,Ctrl+C无效。
根本原因:spin_until_future_complete会启动一个新的事件循环,而服务端的spin()已经在运行一个事件循环。两个循环嵌套,造成死锁。
官方警告原文:“Do not userclpy.spin_until_future_completein a ROS 2 callback.”
修复方案:
- 绝对禁止在任何回调函数(
timer_callback、subscription_callback、service_callback)中调用spin_until_future_complete。 - 正确做法:如果需要在服务端发起另一个服务调用,应使用
self.create_client()创建新客户端,并用call_async()发起异步请求,然后在Future的add_done_callback()中处理结果。例如:
def add_two_ints_callback(self, request, response): # 发起另一个服务调用(如记录日志到远程服务) log_client = self.create_client(LogService, 'log_service') log_req = LogService.Request() log_req.message = f"Calculation: {request.a} + {request.b}" future = log_client.call_async(log_req) future.add_done_callback(self.log_callback) # 在回调中处理日志响应 response.sum = request.a + request.b return response def log_callback(self, future): try: future.result() # 忽略日志响应结果 except Exception as e: self.get_logger().warn('Log service call failed: %r' % e)6. 进阶思考与工程化延伸:从教程Demo到生产系统的跨越
这个加法服务,是ROS 2服务通信的“最小可行原型”(MVP)。但真实机器人系统远比这复杂。基于这个基础,我想分享几个关键的工程化延伸方向,帮你把教程知识转化为解决实际问题的能力。
6.1 从AddTwoInts到自定义服务:如何安全地扩展接口
教程用example_interfaces是捷径,但生产中必然需要自定义服务。比如,你想让加法服务支持浮点数、返回计算耗时、或带错误码。这时,你需要:
- 在自己的包里创建
.srv文件:在~/ros2_ws/src/py_srvcli/py_srvcli/srv/下新建AddFloats.srv:
float64 a float64 b --- float64 sum duration calculation_time uint8 error_code # 0=success, 1=overflow, 2=nan- 在
package.xml中声明服务依赖:
<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_runtime</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>- 在
CMakeLists.txt(如果是C++包)或setup.py(Python包)中启用接口生成:对于Python包,ament_python会自动处理,只需确保pyproject.toml中有[build-system] requires = ["setuptools>=40.8.0", "wheel", "rosidl_python"]。
关键经验:自定义服务的字段名、类型、顺序必须严格一致,否则客户端和服务端解析会错位。建议用ros2 interface show your_package/srv/AddFloats随时验证。
6.2 从单次调用到批量处理:如何让服务支持高并发
教程的客户端每次只发一个请求,但机器人常需批量处理传感器数据。解决方案是:
- 服务端使用
MultiThreadedExecutor:默认SingleThreadedExecutor会串行处理请求,瓶颈明显。在服务端main()中:
rclpy.init() minimal_service = MinimalService() executor = rclpy.executors.MultiThreadedExecutor() executor.add_node(minimal_service) try: executor.spin() except KeyboardInterrupt: pass finally: minimal_service.destroy_node() rclpy.shutdown()- 客户端使用
Future列表:一次性发起多个异步请求,然后用rclpy.spin_until_future_complete轮询所有Future:
futures = [] for a, b in [(1,2), (3,4), (5,6)]: future = client.send_request(a, b) futures.append(future) for future in futures: rclpy.spin_until_future_complete(client, future) response = future.result() print(f"{response.sum}")性能提示:MultiThreadedExecutor会为每个回调分配独立线程,线程数默认为CPU核心数。可通过executor = MultiThreadedExecutor(num_threads=4)控制。
6.3 从命令行参数到参数服务器:如何让服务更灵活
教程用sys.argv传参,但生产环境需支持动态配置。ROS 2的参数系统是答案:
- 服务端声明并获取参数:
def __init__(self): super().__init__('minimal_service') # 声明参数,默认值为100 self.declare_parameter('max_sum', 100) self.max_sum = self.get_parameter('max_sum').value self.srv = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback) def add_two_ints_callback(self, request, response): if request.a + request.b > self.max_sum: response.sum = -1 # 错误码 self.get_logger().warn(f'Sum {request.a + request.b} exceeds max {self.max_sum}') else: response.sum = request.a + request.b return response- 启动时传参:
ros2 run py_srvcli service --ros-args -p max_sum:=50。
优势:参数可热更新(ros2 param set /minimal_service max_sum 200),无需重启节点。
6.4 从Python到C++:为什么有时必须用C++
虽然Python开发快,但某些场景C++