# 30 · Service 深度:同步 req/resp(完全指南) > **目标**:吃透 ROS2 Service,涵盖 .srv 定义、Server/Client API、Future、wait_for_service、调试,学完能写工业级 Service。 --- ## 目录 - [1. 通信模型](#1-通信模型) - [2. .srv 文件定义](#2-srv-文件定义) - [3. Server 端(Python / C++)](#3-server-端python--c) - [4. Client 端(Python / C++)](#4-client-端python--c) - [5. 同步 vs 异步调用](#5-同步-vs-异步调用) - [6. wait_for_service 详解](#6-wait_for_service-详解) - [7. QoS + Service 名约定](#7-qos--service-名约定) - [8. 实战:写一个拍照 + 计算服务](#8-实战写一个拍照--计算服务) - [9. 调试命令](#9-调试命令) - [10. 常见坑](#10-常见坑) - [11. 在本仓库里跑](#11-在本仓库里跑) - [12. 进阶:异步服务端 + 回调式 client](#12-进阶异步服务端--回调式-client) --- ## 1. 通信模型 ### 1.1 一句话 Service 是 **同步、一对一、双向**的请求/响应。 - 一次性调用 + 等结果 - 几毫秒到几秒 ``` Client ──call(req)──> Service Server ◀──response──── ───────────────── 同步(阻塞),一次一答 ``` ### 1.2 vs Topic / Action | 维度 | Service | Topic | Action | |---|---|---|---| | 同步性 | **同步(等响应)** | 异步 | 异步(long) | | 方向 | **双向** req/resp | 单向 pub→sub | 双向 goal/fb/result | | 1对多 | ❌ 一对一 | ✅ | ❌ | | 反馈进度 | ❌ | ❌ | ✅ | | 可取消 | ❌ | N/A | ✅ | ### 1.3 何时用 ✅ **适合**: - 拍照(几 ms) - 关节角度查询 - 短计算(几十 ms) - 开关 / 触发动作 ❌ **不适合**: - 周期性相机帧(用 Topic) - 长任务(用 Action) - 需要进度(用 Action) --- ## 2. .srv 文件定义 ### 2.1 格式 ``` Request 字段 --- Response 字段 ``` ### 2.2 标准 .srv 示例 ```srv # example_interfaces/srv/AddTwoInts.srv int64 a # Request int64 b --- int64 sum # Response ``` ### 2.3 复杂示例(多个字段) ```srv # example_interfaces/srv/SetBool.srv bool data --- bool success string message ``` ### 2.4 本仓库用的 Service `example_interfaces/srv/AddTwoInts`: - Request: `int64 a`, `int64 b` - Response: `int64 sum` --- ## 3. Server 端(Python / C++) ### 3.1 Python ```python 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_py') # create_service(srv_type, srv_name, callback) # callback 签名: callback(request, response) -> response self.srv = self.create_service( AddTwoInts, 'add_two_ints', self.add_two_ints_callback, ) self.get_logger().info('add_two_ints_server ready') def add_two_ints_callback(self, request, response): response.sum = request.a + request.b self.get_logger().info(f'{request.a} + {request.b} = {response.sum}') return response # 必须 return response 对象 def main(): rclpy.init() node = AddTwoIntsServer() try: rclpy.spin(node) except KeyboardInterrupt: pass node.destroy_node() rclpy.shutdown() ``` ### 3.2 C++ ```cpp #include "rclcpp/rclcpp.hpp" #include "example_interfaces/srv/add_two_ints.hpp" class Server : public rclcpp::Node { public: Server() : rclcpp::Node("add_two_ints_server_cpp") { srv_ = this->create_service( "add_two_ints", [this]( const example_interfaces::srv::AddTwoInts::Request::SharedPtr req, example_interfaces::srv::AddTwoInts::Response::SharedPtr res ) { res->sum = req->a + req->b; }); } private: rclcpp::Service::SharedPtr srv_; }; ``` --- ## 4. Client 端(Python / C++) ### 4.1 Python — 同步阻塞风格 ```python class Client(Node): def __init__(self): super().__init__('add_two_ints_client_py') self.client = self.create_client(AddTwoInts, 'add_two_ints') # 阻塞等服务端上线(1s 超时,循环等) while not self.client.wait_for_service(timeout_sec=1.0): self.get_logger().info('waiting for service...') def call_sync(self, a, b): req = AddTwoInts.Request() req.a = a; req.b = b # call_async:返回 Future,不等 future = self.client.call_async(req) # spin_until_future_complete:阻塞到 future 完成(或超时) rclpy.spin_until_future_complete(self, future, timeout_sec=5.0) if future.result() is None: self.get_logger().error('service call failed') return None return future.result().sum ``` ### 4.2 Python — 异步回调风格 ```python class AsyncClient(Node): def __init__(self): super().__init__('async_client') self.client = self.create_client(AddTwoInts, 'add_two_ints') self.client.wait_for_service() def call_async(self, a, b): req = AddTwoInts.Request() req.a = a; req.b = b future = self.client.call_async(req) def done_cb(fut): result = fut.result() if result is not None: self.get_logger().info(f'result: {result.sum}') else: self.get_logger().warn('failed') future.add_done_callback(done_cb) def main(): rclpy.init() node = AsyncClient() node.call_async(12, 30) rclpy.spin(node) # 阻塞,直到 callback 调 shutdown ``` ### 4.3 C++ ```cpp class Client : public rclcpp::Node { public: Client() : rclcpp::Node("client") { client_ = this->create_client("add_two_ints"); while (!client_->wait_for_service(std::chrono::seconds(1))) { RCLCPP_INFO(this->get_logger(), "waiting..."); } } int64_t call(int64_t a, int64_t b) { auto req = std::make_shared(); req->a = a; req->b = b; auto future = client_->async_send_request(req); if (rclcpp::spin_until_future_complete( this->shared_from_this(), future, 5s) == rclcpp::FutureReturnCode::SUCCESS) { return future.get()->sum; } return -1; } }; ``` ### 4.4 关键 API 速查 | Python | C++ | |---|---| | `create_client(SrvType, name)` | `create_client(name)` | | `client.wait_for_service(timeout_sec=N)` | `client->wait_for_service(1s)` | | `client.call_async(req)` | `client->async_send_request(req)` | | `future.result()` | `future.get()` | | `rclpy.spin_until_future_complete(node, future, timeout_sec)` | `rclcpp::spin_until_future_complete(this, future, 5s)` | | `future.add_done_callback(cb)` | (用 `bind`) | --- ## 5. 同步 vs 异步调用 | 方式 | 适用 | 阻塞? | |---|---|---| | `call(req)` | ROS1 风格,**Python 已 deprecated** | ✅ 同步 | | `call_async + spin_until_future_complete` | 命令行 / 单测 / 测试 | ✅ 同步但 yield | | `call_async + add_done_callback` | 生产节点,主线程不能停 | ❌ 异步 | **生产推荐**: `add_done_callback` 风格,主线程继续 spin 其他东西。 --- ## 6. wait_for_service 详解 ### 6.1 为什么需要 ROS2 节点启动到 ROS 实际可达,**需要 1-2 秒 DDS discovery**。 client 启动时 server 可能还没起,所以要先等。 ### 6.2 用法对比 ```python # ❌ 错误:永久阻塞,debug 难 client.wait_for_service() # ⚠️ 不推荐:硬超时 client.wait_for_service(timeout_sec=5.0) # ✅ 推荐:循环 + 日志 while not client.wait_for_service(timeout_sec=1.0): node.get_logger().info('waiting for service...') ``` ### 6.3 死锁陷阱 如果 `wait_for_service()` 在 `__init__` 里**阻塞**,主线程 spin 没跑,DDS discovery 没动 → 永远 wait 不到。 **修法**: 不要在 `__init__` 阻塞,放到独立的 `wait_for_server_ready()` 方法。 --- ## 7. QoS + Service 名约定 ### 7.1 Service QoS 默认 RELIABLE。一致即可,不用改。 ### 7.2 命名约定 | 角色 | 推荐节点名 / service 名 | |---|---| | Service Server | `_server` (如 `add_two_ints_server`) | | Service Client | `_client` (如 `add_two_ints_client`) | | Service 名 | `` (如 `add_two_ints`、`take_photo`) | --- ## 8. 实战:写一个拍照 + 计算服务 ### 8.1 .srv 定义(自定义) ```srv # my_camera/srv/TakePhoto.srv string filename --- bool success int32 width int32 height string saved_path ``` ### 8.2 Server ```python import cv2 from my_camera.srv import TakePhoto class TakePhotoServer(Node): def __init__(self): super().__init__('take_photo_server') self.srv = self.create_service( TakePhoto, 'take_photo', self.cb) def cb(self, req, resp): # 1) 从相机读 frame cap = cv2.VideoCapture(0) ret, frame = cap.read() if not ret: resp.success = False return resp # 2) 保存 h, w = frame.shape[:2] path = f'/tmp/{req.filename}.jpg' cv2.imwrite(path, frame) resp.success = True resp.width = w resp.height = h resp.saved_path = path return resp ``` ### 8.3 Client ```python class TakePhotoClient(Node): def take(self, filename): req = TakePhoto.Request() req.filename = filename future = self.client.call_async(req) rclpy.spin_until_future_complete(self, future) result = future.result() if result.success: print(f'saved {result.saved_path} ({result.width}x{result.height})') ``` --- ## 9. 调试命令 ```bash ros2 service list # 所有 service ros2 service type /add_two_ints # 服务类型 ros2 service info /add_two_ints -v # 提供方节点 ros2 service find example_interfaces/srv/AddTwoInts # 找某类型的所有 service ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 12, b: 30}" ``` --- ## 10. 常见坑 ### 10.1 Future 不 done ```python # 错:立即读 future.result() future = client.call_async(req) result = future.result() # 阻塞到死 # 对:spin_until_future_complete rclpy.spin_until_future_complete(node, future, timeout_sec=5.0) result = future.result() ``` ### 10.2 服务端 shutdown 后 client 调 ```python # 服务端 rclpy.shutdown() 后,future.result() 是 None assert future.result() is not None, 'service unavailable' ``` ### 10.3 多 client 排队 Service 是 1对1。两个 client 同时调,**第二个等第一个完成**。长任务用 Action。 ### 10.4 wait_for_service 死锁 不要在 `__init__` 阻塞 wait!否则 DDS discovery 没法跑。 --- ## 11. 在本仓库里跑 ### 11.1 启 server ```bash docker exec ros2_dev bash -lc "source /root/ros2_ws/install/setup.bash && ros2 launch bringup service_launch.py" ``` ### 11.2 调 service(新终端) ```bash docker exec ros2_dev bash -lc "source /root/ros2_ws/install/setup.bash && ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts '{\"a\": 12, \"b\": 30}'" ``` **预期输出**: ``` response: example_interfaces.srv.AddTwoInts_Response(sum=42) ``` ### 11.3 源码 - Server: [`src/py_srv/py_srv/add_two_ints_server.py`](../src/py_srv/py_srv/add_two_ints_server.py) - Client: [`src/py_srv/py_srv/add_two_ints_client.py`](../src/py_srv/py_srv/add_two_ints_client.py) - launch: [`src/bringup/launch/service_launch.py`](../src/bringup/launch/service_launch.py) - 测试: [`src/py_srv/test/test_srv.py`](../src/py_srv/test/test_srv.py) ### 11.4 端到端日志 [`docker/srv_e2e.log`](../docker/srv_e2e.log) --- ## 12. 进阶:异步服务端 + 回调式 client ### 12.1 异步服务端 ```python class AsyncServer(Node): """长时间运行的服务,内部用回调推进,不阻塞主线程。""" def __init__(self): super().__init__('async_server') self.srv = self.create_service(MySrv, 'async_srv', self.cb) def cb(self, req, resp): # 启动后台任务做实际工作 future = self._do_work(req) future.add_done_callback(lambda f: self._respond(f, resp)) return resp # 先返回,后续异步填充 def _do_work(self, req): # 用 executor.spin_until_future_complete 做异步 ... ``` ### 12.2 回调式 client ```python def on_response(future): result = future.result() print(f'result: {result.value}') rclpy.shutdown() future = client.call_async(req) future.add_done_callback(on_response) rclpy.spin(node) ``` --- ## Service vs Action 怎么选 | 维度 | Service | Action | |---|---|---| | 持续时间 | < 几秒 | 几秒 ~ 几小时 | | 反馈进度 | ❌ | ✅ Feedback | | 可取消 | ❌ | ✅ | | 适用 | 拍照、查询 | 抓取、导航 | 本仓库 [`py_srv`](../src/py_srv/) 是 Service demo;[`py_action_demo`](../src/py_action_demo/) 是 Action demo。 --- ## 接下来读 | 主题 | 文档 | |---|---| | Topic 深度 | [`20-topics.md`](20-topics.md) | | Action 深度 | [`40-actions.md`](40-actions.md) | | TF2 坐标变换 | [`50-tf2.md`](50-tf2.md) | | 三机部署 | [`100-embedded-deployment.md`](100-embedded-deployment.md) | --- --- ## 📖 阅读路径导航 > 💡 这是仓库 `doc/` 下所有文档的推荐阅读顺序。[返回 README 总导航](../README.md#-23-篇文档怎么读) > > ⏱ **本文预计阅读时间**: 50 分钟 > 📍 **当前位置**: 第 14 / 24 篇 - ⏮ **上一篇**: [DDS 配置 + colcon overlay](../21-overlay-dds.md) - ⏭ **下一篇**: [Action 深度](../40-actions.md)