ROS 2 Python服务通信详解:从AddTwoInts到生产级实践
2026/7/13 10:28:37 网站建设 项目流程

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.RequestAddTwoInts.Response),并注入到rclpy的序列化/反序列化管道中。这意味着:

  • 服务端response.sum = request.a + request.b能直接赋值,是因为生成的Response类里真有sum这个属性,且类型是int64(对应Python的int);
  • 客户端self.req.a = a能成功,是因为Request类里真有ab这两个字段
  • 如果你手写一个MyAdd.srv,哪怕只是把int64改成int32,ROS 2也会在编译时报错,因为C++后端生成的类型不匹配。

提示:example_interfaces是ROS 2官方预置的“标准接口库”,类似Linux的libc。它包含AddTwoIntsTriggerSetBool等常用服务定义。不要自己造轮子去重写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合法,AddTwoIntsadd-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.pyentry_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.xmlinstall/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.pypackages=find_packages()没包含py_srvcli子目录,导致colcon buildinstall/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 Node
  • from example_interfaces.srv import AddTwoInts:导入的是生成的服务类,不是.srv文件本身。ROS 2的ament_cmake_pythonsetuptools在构建时,会扫描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 response
  • response.sum = request.a + request.b:这里没有类型转换,因为request.arequest.bint64,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/exceptfinally块里,否则Ctrl+C后资源无法释放。

3.2 客户端代码深度解析:client_member_function.py

import sys from example_interfaces.srv import AddTwoInts import rclpy from rclpy.node import Node
  • import sys:用于读取命令行参数(sys.argv[1]sys.argv[2])。这是客户端最简化的输入方式,生产环境应替换为declare_parameterget_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()是一个空壳,ab字段初始为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对象封装了请求的状态(pendingdonecancelled)和最终结果。
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.TimeoutExceptionrclpy.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.xmlsetup.pysetup.cfgpyproject.toml等文件;cat ~/ros2_ws/src/py_srvcli/package.xml | grep example_interfaces应输出<depend>example_interfaces</depend>,证明依赖已自动添加。

4.2 配置文件精细化修改

目的:让包符合ROS 2规范,支持colcon buildros2 run
操作

  1. 编辑~/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>
  1. 编辑~/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.pyentry_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,应列出serviceclient两个可执行项。

4.3 服务端与客户端代码编写

目的:实现加法逻辑,确保代码结构符合ROS 2 Python最佳实践。
操作

  1. 创建服务端文件:
touch ~/ros2_ws/src/py_srvcli/py_srvcli/service_member_function.py # 将教程中的完整代码粘贴进去
  1. 创建客户端文件:
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.pymain()函数中,必须添加异常处理
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调用失败,但错误被静默吞掉。
排查步骤

  1. 检查服务端是否真的在运行:执行ps aux | grep service_member_function,如果无输出,说明进程已退出。
  2. 手动运行服务端看报错:在服务端终端执行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→ 服务名含非法字符(如大写字母、短横线)。
  3. 检查ROS 2域ID一致性:执行echo $ROS_DOMAIN_ID,确保服务端和客户端终端输出相同。默认为0,但若某终端设置了export ROS_DOMAIN_ID=1,它们就无法通信。

修复方案:统一source install/setup.bash,确保ROS_DOMAIN_ID一致,修正服务名。

5.2 问题:客户端收到响应,但response.sum0,而非预期的和

现象:客户端日志显示Result of add_two_ints: for 2 + 3 = 0,服务端日志显示Incoming request a: 2 b: 3
根本原因:服务端回调函数中,response.sum = request.a + request.b执行了,但return response被遗漏或写在了条件分支外。
排查步骤

  1. 检查服务端代码:确认add_two_ints_callback函数末尾有return response,且不在if语句内。
  2. 添加调试日志:在return response前加一行self.get_logger().info('Calculated sum: %d' % response.sum),确认计算值正确。
  3. 检查.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>缺失。
排查步骤

  1. 检查setup.py格式:确认entry_points是字典,键为'console_scripts',值为字符串列表,且逗号后有空格('service = ...',)。Python对缩进和逗号极其敏感。
  2. 检查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是构建时依赖,两者都要有。
  3. 检查构建产物:执行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的服务发现有缓存机制,客户端可能还记着旧的服务地址。
排查步骤

  1. 清除ROS 2参数缓存:执行ros2 param set /minimal_client_async use_sim_time false(任意参数),强制刷新节点状态。
  2. 重启DDS中间件:执行killall -9 rmw_implementation(针对Fast DDS)或killall -9 cyclonedds(针对Cyclone DDS),然后重启服务端和客户端。
  3. 检查网络连接:执行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_callbacksubscription_callbackservice_callback)中调用spin_until_future_complete
  • 正确做法:如果需要在服务端发起另一个服务调用,应使用self.create_client()创建新客户端,并用call_async()发起异步请求,然后在Futureadd_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是捷径,但生产中必然需要自定义服务。比如,你想让加法服务支持浮点数、返回计算耗时、或带错误码。这时,你需要:

  1. 在自己的包里创建.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
  1. 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>
  1. 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++

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询