510 lines
13 KiB
Markdown
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) | |