1. 为什么机器人开发不能只靠“一问一答”
先抛一个场景:你在跑一个 ROS2 的导航程序,机器人走到一半,你突然想知道当前里程计累计走了多少米,或者想让某个传感器立即回传一次数据。这种场景下,用话题(Topic)去广播一轮,太啰嗦;把数据存在参数服务器里,又不够实时。ROS2 给的标准答案是:服务(Service)。
服务是 ROS2 四大核心通信机制之一,其余三个分别是话题、动作(Action)和参数。如果你刚接触 ROS2,可能已经把话题玩得很溜了,但服务这块往往容易模糊——它到底和话题有什么区别?什么时候该用服务而不是话题?服务的数据格式怎么定义?服务调用超时怎么办?这些问题我当初都踩过坑,这篇文章就把服务这个核心概念彻底拆开讲清楚。
这篇文章适合谁看:正在学 ROS2 的初学者、已经从 ROS1 迁移到 ROS2 的开发者、以及想搞清楚服务通信底层原理的机器人工程师。我会从服务的设计思路讲到代码实现,再讲排查技巧,最后给出一套可以直接抄作业的最佳实践。不管你是用 Humble 还是 Iron 版本,核心思路完全一致。
2. 服务通信的设计思路与话题的分工逻辑
2.1 服务是什么:一次请求,一次响应
服务的本质是同步的请求-响应模式。一个节点扮演服务端(Service Server),负责接收请求并返回结果;另一个节点扮演客户端(Service Client),负责发起请求并等待响应。整个过程是短连接式的:客户端发一个请求,服务端处理完,返回一个响应,通信结束。
这和话题完全不同。话题是发布-订阅模式,发布者持续广播,订阅者被动接收,双方不需要知道对方是谁。而服务是点对点通信,客户端必须明确知道服务端的存在,并指定服务名称来发起调用。
我习惯用一个生活化类比:话题像是电台广播,你打开收音机就能听,但没人管你有没有听到;服务像是打电话,你拨号、对方接听、你问一句、对方答一句,整个对话有来有回。前者适合持续数据流,比如传感器数据、速度指令;后者适合偶发的、需要确认结果的操作,比如“拍一张照片”“计算一个路径”“读取当前状态”。
2.2 为什么不用话题硬扛所有通信
很多初学者会问:既然话题这么灵活,为什么还要服务?我用一个真实例子说明。假设你写了一个机器人底盘驱动节点,需要支持“原地旋转90度”这个操作。如果用话题实现,你得定义两个话题:一个发指令、一个回报结果。麻烦在于结果怎么判断?你得在回报话题里不断监听状态,再自己维护一套状态机来判断动作是否完成。如果是服务,整个过程就是:调用服务、传入旋转角度、服务端执行完返回一个布尔值,告诉你成功还是失败。代码逻辑清晰得多。
再考虑一个更本质的问题:话题的消息是单向的、无状态的。服务天然具备事务性——调用要么成功,要么失败,这是工程上非常需要的语义。比如机器人请求一个抓取任务,如果抓取失败,客户端能立即得到反馈并决定下一步策略,而不需要靠超时去猜。
2.3 服务在什么场景下是首选
根据我的实际项目经验,下面这些场景优先选服务:
- 查询类操作:获取当前电池电量、获取机器人位姿、获取地图中的某个坐标点信息。
- 控制类操作:打开/关闭某个执行器、切换到某个导航目标点、启动/停止某个算法模块。
- 计算类操作:路径规划、点云处理、图像特征提取。
- 需要确认结果的操作:比如“保存地图”这种任务,你必须知道保存是否成功。
如果是连续的数据流、高频的周期性数据、一对多的广播场景,那还是老老实实用话题。服务和话题没有孰优孰劣,只有合不合适。
3. 核心细节解析:服务接口定义与通信协议层
3.1 服务接口的构成:请求与响应
服务的通信内容由srv 文件定义。一个 srv 文件由三部分组成:请求部分(Request)、分隔符---、响应部分(Response)。以 ROS2 内置的标准服务AddTwoInts为例,它的定义就是这样:
int64 a int64 b --- int64 sum请求部分有两个字段a和b,响应部分有一个字段sum。这个接口绝对清晰:客户端传入两个整数,服务端返回它们的和。
在实际项目中,你经常会自定义服务接口。比如设计一个“获取机器人当前角度”的服务,srv 文件可以这样写:
# 请求部分,可以留空 --- # 响应部分 float32 yaw float32 pitch float32 roll请求部分留空意味着客户端只需要触发调用,不需要传任何参数。这种设计在处理“查询当前状态”类需求时特别好用。
3.2 接口命名规范与包管理
自定义 srv 接口时,建议放在独立的接口包中,比如my_robot_interfaces。这样做的好处是:客户端和服务端节点都依赖同一个接口包,避免了接口定义不一致导致的诡异问题。接口包的结构通常长这样:
my_robot_interfaces/ ├── CMakeLists.txt ├── package.xml ├── msg/ │ └── MyMessage.msg └── srv/ └── GetRobotState.srv在CMakeLists.txt中需要添加依赖,并显式声明要生成的 srv 文件:
find_package(rosidl_default_generators REQUIRED) rosidl_generate_interfaces(${PROJECT_NAME} "srv/GetRobotState.srv" )在package.xml中添加:
<build_depend>rosidl_default_generators</build_depend> <exec_depend>rosidl_default_generators</exec_depend> <member_of_group>rosidl_interface_packages</member_of_group>这一步最容易出问题。很多初学者把 srv 文件写好了,但忘了改CMakeLists.txt里的rosidl_generate_interfaces参数,结果编译后找不到接口。编译接口包时,最好在终端里用colcon build --packages-select my_robot_interfaces单独构建该包,看到[Processing: my_robot_interfaces]相关的日志后再构建其他包。
3.3 服务通信协议层:底层到底是怎么传的
ROS2 服务通信的协议层基于RMW(ROS Middleware)实现,默认的 RMW 是DDS(Data Distribution Service)。DDS 本身就是一套完整的发布-订阅通信中间件,ROS2 的很多特性(比如 QoS、动态发现)其实都来自 DDS。服务通信在 DDS 的框架下,本质上也是借助 DDS 的底层数据通道来实现请求和响应的配对。
服务通信在协议栈上有一个重要特性:底层使用临时话题来实现请求和响应。当服务被创建时,RMW 会自动为这个服务创建两个内部话题:一个用于客户端发送请求,一个用于服务端返回响应。这个细节解释了为什么服务调用有时看起来不太像一个“直接的连接”——因为它确实不是同一条通道上完成的请求响应,而是通过两个隐藏的话题协作完成的。
对普通开发者来说,你不需要亲自处理这些底层细节,但理解这一点能帮助你排查一些奇怪的问题。比如,如果你在ros2 topic list里看到一些类似/service_name/_service_requests的内部话题,不要惊讶,那是服务通信协议层的正常表现。
3.4 QoS 策略在服务里的特殊规则
服务通信有自己的 QoS 策略规则,这一点经常被忽略。对于服务端,请求的 QoS 和响应的 QoS 是独立的;对于客户端,也一样。默认情况下,绝大多数 RMW 实现为服务配置的是transient local和reliable的组合。
我记得有一次在工程现场,客户端调用服务频繁超时,后来发现是我把服务端的 QoS 历史深度策略改成了KEEP_LAST且队列深度设为 1,而客户端那边设置的请求频率又特别高,导致请求在 DDS 层被丢弃。通常不建议手动修改服务的 QoS 策略,保持默认值是最稳妥的。这个话题很深,但你现在只需要记住:服务的 QoS 配置比话题更敏感,没有充分理由不要改它。
4. 实操过程:用 Python 和 C++ 实现一个完整服务通信
4.1 环境准备与项目结构
在动手写代码前,先确认你的 ROS2 环境已经装好。这里以 Ubuntu 22.04 搭配 ROS2 Humble 为例。安装完成后的验证命令:
source /opt/ros/humble/setup.bash ros2 --version创建工作空间并创建功能包:
mkdir -p ~/ros2_ws/src cd ~/ros2_ws/src ros2 pkg create --build-type ament_python py_srv_demo ros2 pkg create --build-type ament_cmake cpp_srv_demo如果你打算先做接口包,就在同一个src目录下创建接口包:
ros2 pkg create --build-type ament_cmake my_robot_interfaces4.2 服务端实现:Python 版
我用 Python 写一个加法服务端,使用内置的AddTwoInts服务接口。这个例子虽然简单,但能覆盖服务端开发的所有关键点。
创建py_srv_demo/py_srv_demo/server.py文件:
import rclpy from rclpy.node import Node from example_interfaces.srv import AddTwoInts class AddTwoIntsServer(Node): def __init__(self): super().__init__('add_two_ints_server') self.srv = self.create_service( AddTwoInts, 'add_two_ints', self.add_two_ints_callback) self.get_logger().info('服务端已启动,等待客户端调用...') def add_two_ints_callback(self, request, response): self.get_logger().info( f'收到请求: a={request.a}, b={request.b}') response.sum = request.a + request.b self.get_logger().info(f'返回结果: sum={response.sum}') return response def main(args=None): rclpy.init(args=args) node = AddTwoIntsServer() try: rclpy.spin(node) except KeyboardInterrupt: pass finally: node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()这里有几个细节值得注意:
第一,create_service的回调函数必须返回响应对象。如果你在回调里忘了return response,客户端会一直等待,直到超时。
第二,服务回调是阻塞运行的吗?不完全是。在 ROS2 的默认 executor 模式下,多个服务回调会串行执行。如果服务端有多个服务,某个回调执行时间过长,会阻塞其他回调。需要并行处理时,可以考虑使用多线程 executor。
第三,服务名称add_two_ints是全局的,在同一台机器上不能有两个服务端同时注册同名服务,否则会发生竞态。
4.3 客户端实现:Python 版
创建py_srv_demo/py_srv_demo/client.py文件:
import sys import rclpy from rclpy.node import Node from example_interfaces.srv import AddTwoInts class AddTwoIntsClient(Node): def __init__(self): super().__init__('add_two_ints_client') self.cli = self.create_client(AddTwoInts, 'add_two_ints') while not self.cli.wait_for_service(timeout_sec=1.0): self.get_logger().warn('等待服务端上线...') self.req = AddTwoInts.Request() def send_request(self, a, b): self.req.a = a self.req.b = b self.future = self.cli.call_async(self.req) rclpy.spin_until_future_complete(self, self.future) if self.future.result() is not None: return self.future.result().sum else: self.get_logger().error('服务调用失败') return None def main(): rclpy.init() node = AddTwoIntsClient() a = int(sys.argv[1]) b = int(sys.argv[2]) result = node.send_request(a, b) if result is not None: node.get_logger().info(f'{a} + {b} = {result}') node.destroy_node() rclpy.shutdown() if __name__ == '__main__': main()客户端的核心步骤是:等待服务上线、构造请求、异步调用、等待结果。wait_for_service这一步非常关键,如果服务端还没启动,客户端直接调用会抛异常。等待时间可以根据业务需要调整,默认写 1 秒轮询一次比较合理。
4.4 C++ 版服务端与客户端
C++ 版本在语法上比 Python 更繁琐,但性能更好。在cpp_srv_demo/src目录下创建server.cpp:
#include <rclcpp/rclcpp.hpp> #include <example_interfaces/srv/add_two_ints.hpp> using namespace std::chrono_literals; class AddTwoIntsServer : public rclcpp::Node { public: AddTwoIntsServer() : Node("add_two_ints_server") { server_ = this->create_service<example_interfaces::srv::AddTwoInts>( "add_two_ints", std::bind(&AddTwoIntsServer::handle_request, this, std::placeholders::_1, std::placeholders::_2)); RCLCPP_INFO(this->get_logger(), "C++ 服务端已启动"); } private: void handle_request( const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request, std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response) { response->sum = request->a + request->b; RCLCPP_INFO(this->get_logger(), "收到请求: %ld + %ld = %ld", request->a, request->b, response->sum); } rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr server_; }; int main(int argc, char **argv) { rclcpp::init(argc, argv); auto node = std::make_shared<AddTwoIntsServer>(); rclcpp::spin(node); rclcpp::shutdown(); return 0; }C++ 服务端要注意的是回调函数签名:handle_request接受Request和Response的智能指针。C++ 里 Response 是服务端直接实例化的,不需要你手动返回,修改 Response 指针指向的内容即可。
C++ 客户端创建client.cpp:
#include <rclcpp/rclcpp.hpp> #include <example_interfaces/srv/add_two_ints.hpp> using namespace std::chrono_literals; class AddTwoIntsClient : public rclcpp::Node { public: AddTwoIntsClient() : Node("add_two_ints_client") { client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints"); while (!client_->wait_for_service(1s)) { if (!rclcpp::ok()) { RCLCPP_ERROR(this->get_logger(), "客户端被中断"); return; } RCLCPP_WARN(this->get_logger(), "等待服务端上线..."); } } int call_add(int a, int b) { auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>(); request->a = a; request->b = b; auto future = client_->async_send_request(request); if (rclcpp::spin_until_future_complete(this->shared_from_this(), future) == rclcpp::FutureReturnCode::SUCCESS) { return future.get()->sum; } else { RCLCPP_ERROR(this->get_logger(), "服务调用失败"); return -1; } } private: rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_; }; int main(int argc, char **argv) { rclcpp::init(argc, argv); auto node = std::make_shared<AddTwoIntsClient>(); int result = node->call_add(3, 5); RCLCPP_INFO(node->get_logger(), "3 + 5 = %d", result); rclcpp::shutdown(); return 0; }C++ 客户端有个常见坑:async_send_request的 future 必须等spin_until_future_complete返回成功后才能读取结果,如果直接调用future.get()可能遇到 future 尚未就绪的情况。
4.5 编译、运行与命令行验证
先玩一遍命令行工具,这是调试服务最有效的方式。启动一个服务端后,在另一个终端里查看服务列表:
ros2 service list查看服务类型:
ros2 service type /add_two_ints直接调用服务,相当于手动扮演客户端:
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 10, b: 20}"如果一切正常,你会看到类似输出:
waiting for service to become available... response: example_interfaces.srv.AddTwoInts_Response(sum=30)这里有个小技巧:使用ros2 service call时,参数必须用双引号包住整个 JSON 内容,并且字段名要严格匹配 srv 定义。字段名拼错一个字,调用就会失败。这个工具在排查服务端逻辑问题时尤其好用——你可以完全不写客户端代码,先验证服务端是否正常工作。
5. 工具选型解析:服务调试三板斧
5.1ros2 service命令族的完整用法
ros2 service是排查服务通信问题的首选工具,它的子命令包括:
| 子命令 | 作用 |
|---|---|
ros2 service list | 列出当前所有可用服务 |
ros2 service type <service_name> | 查看指定服务的接口类型 |
ros2 service find <type_name> | 按接口类型查找服务 |
ros2 service call <service_name> <type_name> <arguments> | 手动调用服务 |
ros2 service list -t | 同时列出服务名和类型 |
还有一个高阶用法:查看服务端和客户端的节点关系。启动服务端和客户端后,用rqt_graph查看节点图,你会看到服务端和客户端之间有一条边,边上的标签正是服务名。这条边对应的是服务连接,而不是话题连接。
5.2ros2 interface命令族:快速查看接口定义
调试服务接口时,ros2 interface命令帮了很大忙。查看内置服务接口的定义:
ros2 interface show example_interfaces/srv/AddTwoInts查看自定义接口包中的服务:
ros2 interface show my_robot_interfaces/srv/GetRobotState这个命令会直接输出 srv 文件的内容,不用去翻源码。如果你想验证接口包编译是否成功,先source install/setup.bash,再执行这个命令,能正常输出就说明接口已经被正确注册到系统中。
5.3rqt_service_caller图形化调试
如果你不喜欢纯命令行的交互方式,ROS2 还提供了一个图形化工具rqt_service_caller。安装命令:
sudo apt install ros-humble-rqt-service-caller运行:
rqt_service_caller在这个工具里,你可以下拉选择服务,工具会自动读取服务接口定义并生成参数输入框,填入参数后点击 Call 就能调用服务。这个工具的好处是直观,适合演示或者快速验证服务接口是否符合预期。
6. 常见问题与排查技巧实录
6.1 服务调用超时:服务端没上线还是网络隔离
我遇到最多的服务通信问题就是调用超时。客户端调用服务后一直等待,最后报出超时错误。
排查思路按优先级排列:
- 在客户端所在终端执行
ros2 service list,确认服务是否可见。如果不可见,说明客户端和服务端不在同一通信域,或者服务端根本没起来。 - 在服务端所在终端确认服务端节点状态,观察有没有报错日志。
- 如果服务端和客户端分布在不同机器上,检查
ROS_DOMAIN_ID是否一致。域 ID 不同会导致双方互相看不到对方。 - 检查防火墙。ROS2 的 DDS 通信依赖多种端口,如果防火墙阻止了多播或对端端口,服务发现就会失败。
有一个排查小技巧:用ros2 doctor命令检查 ROS2 环境是否健康,它会自动检测环境变量、网络设置和依赖问题。
6.2 服务端重启后客户端一直等待
有一种场景很典型:服务端因为某种原因重启了,客户端在启动时已经绑定过服务,重启后客户端不会自动重新发现服务,导致后续调用一直等待。
解决方案是在客户端的调用逻辑里加入重连机制。不要只在构造函数里检查一次服务可用状态,而是在每次调用前都检查:
if not self.cli.service_is_ready(): self.get_logger().warn('服务不在线,重新等待...') self.cli.wait_for_service(timeout_sec=2.0)实测下来,这种策略能显著提升系统的鲁棒性。特别是在机器人实际运行中,某个功能模块动态重启是很常见的事。
6.3 多线程服务端与回调阻塞问题
默认的 single-threaded executor 中,所有回调都是串行的。如果一个服务回调里做了耗时操作(比如处理点云、等待硬件响应),其他服务回调就会被堵住。这时需要用多线程 executor:
from rclpy.executors import MultiThreadedExecutor from rclpy.callback_groups import ReentrantCallbackGroup rclpy.init() node = MyNode() executor = MultiThreadedExecutor(num_threads=4) executor.add_node(node) executor.spin()同时,在创建服务时指定回调组:
self.srv = self.create_service( AddTwoInts, 'add_two_ints', self.callback, callback_group=ReentrantCallbackGroup())这里要注意,ReentrantCallbackGroup表示同一组里的回调可以并发执行,这对成员变量的线程安全提出了要求。如果你的服务回调里修改了共享状态,务必加锁保护。我见过太多因为多线程回调并发修改同一变量导致的数据竞争问题。
6.4 服务接口不匹配导致调用失败
当你用ros2 service call调用服务时,接口类型必须完全一致,包括包名和类型名。比如:
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 1, b: 2}"这里的接口类型是example_interfaces/srv/AddTwoInts,少写一个包名前缀就会失败。这个问题在自定义接口时特别容易踩坑,因为自定义接口的包名通常比较长,手打容易出错。
6.5 服务调用频率过高被 DDS 丢弃
当客户端以非常高的频率调用服务时,可能会出现偶发的调用失败或超时。这通常是 DDS 层排队策略导致的。服务请求是按 QoS 策略排队的,默认队列深度有限。
解决方案有两种:一是降低调用频率,在实际工程中,大多数服务并不需要高频调用;二是检查服务端的 QoS 设置,确保历史深度策略能够容纳预期的并发调用。记住之前的建议:没有充分理由不要动 QoS。
6.6 实战速查表
| 现象 | 可能原因 | 解决方案 |
|---|---|---|
| 服务列表里看不到服务 | 服务端未启动、域 ID 不一致、网络隔离 | 检查ros2 service list、统一ROS_DOMAIN_ID、检查防火墙 |
| 客户端调用超时 | 服务端崩溃、回调阻塞、服务名称错误 | 确认服务端进程存活,检查服务端日志 |
| 调用返回但值不对 | srv 接口定义不匹配 | 使用ros2 interface show对比接口定义 |
| 服务端日志显示收到了请求,但客户端超时 | 响应对象未正确填充或未返回 | 确保回调中返回了填充完成的响应对象 |
| 多服务同时调用卡顿 | 单线程 executor 顺序执行 | 改用多线程 executor |
7. 从服务到微服务架构:ROS2 服务的宏观视角
你可能注意到热搜词里出现了“微服务架构”。ROS2 的服务设计理念和微服务架构确实有异曲同工之处。在微服务架构中,各个服务独立部署、独立扩展,通过轻量级通信机制互相协作;ROS2 中,每个节点可以看作一个 mini 服务,服务通信则是它们之间协作的一种方式。
在机器人软件架构中,这种“服务化”思路很有实际价值。比如一个完整的机器人系统,可以拆成“底盘服务”“导航服务”“视觉服务”“语音服务”等模块。每个模块暴露一组标准服务接口,其他模块通过服务调用完成交互。这样做的好处是模块之间解耦,单个模块可以独立替换、独立调试、独立重启。
我参与过一个仓储机器人的项目,最初所有逻辑都写在一个大节点里,改一个功能要重启整个节点,调试效率极低。后来按服务化思路重构,把底层硬件控制、路径规划、任务调度拆成独立节点,节点之间用服务通信,整个系统清晰了很多,故障排查也方便——哪个服务挂了,日志里一目了然。
服务通信本身没有魔法,但它背后蕴含的“各司其职、标准接口、可替换实现”思想,正是大型机器人软件架构的基石。
8. 实际工程中的服务选型建议
服务通信的工程实践,远比教条式的“接口定义+调用”复杂。根据我在多个机器人项目中的经验,这里给出几条务实建议。
第一,服务的粒度要适中。如果一个服务接口的参数超过五六个、响应字段超过七八个,往往说明服务职责过重。考虑拆分成多个小服务。反过来,如果服务调用频率极高且每次只传一个整数,也要考虑是不是应该用话题。
第二,客户端要有超时处理和重试机制。这在第 6 章已经详细说明。永远不要把服务调用当作“必然成功”的操作——机器人运行环境中,节点重启、网络抖动、硬件故障都可能发生。
第三,合理使用同步调用与异步调用。call_async是异步调用,适合在回调函数中调用服务;call是同步调用(Python 中为call的同步版本,C++ 中通过async_send_request加等待实现),适合在独立线程中调用。在回调函数中做同步调用,容易导致死锁或者阻塞 executor。
第四,记录服务调用的关键日志。服务端记录收到请求的参数和返回结果,客户端记录调用耗时和结果状态。这些日志在排查问题时价值巨大。我习惯在服务端日志里加上调用者信息,虽然 ROS2 没有直接提供,但可以在请求参数里带上 client_id 字段。
第五,服务接口一经发布,尽量保持兼容。接口变更会导致所有客户端同步修改。机器人系统往往涉及多个模块协同,接口变动的影响范围可能远超预期。新增字段时考虑给默认值,删除字段时要格外谨慎。
9. 手上同时有 ROS1 经验的话,这里有几点迁移提示
如果你是从 ROS1 迁过来的,服务的核心概念差别不大,但实现细节有差异:
ROS1 的服务端使用ros::ServiceServer,ROS2 使用rclcpp::Service;ROS1 的回调返回布尔值,ROS2 的回调返回 Response 对象。ROS1 的客户端调用是call,ROS2 是async_send_request。ROS2 引入了wait_for_service机制,这在 ROS1 中是没有的——这也算是一个改进,让客户端可以优雅地处理服务端未就绪的情况。
ROS2 服务还有一个 ROS1 没有的特性:服务名支持命名空间。在多个机器人协同的场景下,可以为每个机器人设置不同的命名空间,这样每个机器人都可以有自己的同名服务,互不干扰。
还有一个经常被忽略的差异:ROS2 服务的发现机制依赖 DDS 的动态发现,如果网络环境禁止多播(比如某些工业现场),服务发现可能失败。这种情况下需要配置 DDS 的发现服务器(Discovery Server),把发现机制从多播切换为单播。这个话题展开很多,但至少你要知道有这个问题存在。
10. 实操心得:我最常用的一套服务调试流程
最后分享一个我实际开发中经常使用的完整调试流程,你可以直接照做。
第一步,用命令行快速验证服务端。启动服务端节点后,用ros2 service list确认服务已经注册,用ros2 service call手动调用确认返回结果正确。
第二步,验证客户端。启动客户端节点,观察能否正常调用。如果超时,按第 6 章的排查表逐步检查。
第三步,用rqt_graph检查节点连接。确认服务端和客户端之间没有多余的中转节点。
第四步,在代码中加入关键日志。包括服务端收到请求的时间、处理耗时、返回结果;客户端发送请求的时间、等待耗时、最终结果。
第五步,做异常测试。杀掉服务端进程,看客户端是否能正确报错或等待;重启服务端,看客户端能否恢复连接。这一步虽然费时间,但在真实环境中价值极大。
这个流程看起来简单,但能覆盖绝大多数服务通信问题。我见过不少开发者,花了大量时间在写代码上,出了问题却不知道从哪查起。其实只要掌握了这套验证方法,大部分问题都能在十分钟内定位。
服务这个机制,本身不复杂,但它在 ROS2 中的角色非常关键。搞懂了服务,你就掌握了机器人系统中“请求-应答”这类交互模式的标准解法。下一步可以继续学动作(Action),它是服务在长时间任务场景下的延伸,理解了服务再学动作,会轻松很多。