Files
ROS2_learn/doc/30-services.md
T
2026-08-03 18:09:35 +08:00

510 lines
13 KiB
Markdown

# 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<example_interfaces::srv::AddTwoInts>(
"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<example_interfaces::srv::AddTwoInts>::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<example_interfaces::srv::AddTwoInts>("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<example_interfaces::srv::AddTwoInts::Request>();
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<T>(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 | `<verb>_server` (如 `add_two_ints_server`) |
| Service Client | `<verb>_client` (如 `add_two_ints_client`) |
| Service 名 | `<action>` (如 `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) |