13 KiB
13 KiB
30 · Service 深度:同步 req/resp(完全指南)
目标:吃透 ROS2 Service,涵盖 .srv 定义、Server/Client API、Future、wait_for_service、调试,学完能写工业级 Service。
目录
- 1. 通信模型
- 2. .srv 文件定义
- 3. Server 端(Python / C++)
- 4. Client 端(Python / C++)
- 5. 同步 vs 异步调用
- 6. wait_for_service 详解
- 7. QoS + Service 名约定
- 8. 实战:写一个拍照 + 计算服务
- 9. 调试命令
- 10. 常见坑
- 11. 在本仓库里跑
- 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 示例
# example_interfaces/srv/AddTwoInts.srv
int64 a # Request
int64 b
---
int64 sum # Response
2.3 复杂示例(多个字段)
# 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
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++
#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 — 同步阻塞风格
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 — 异步回调风格
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++
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 用法对比
# ❌ 错误:永久阻塞,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 定义(自定义)
# my_camera/srv/TakePhoto.srv
string filename
---
bool success
int32 width
int32 height
string saved_path
8.2 Server
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
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. 调试命令
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
# 错:立即读 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 调
# 服务端 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
docker exec ros2_dev bash -lc "source /root/ros2_ws/install/setup.bash && ros2 launch bringup service_launch.py"
11.2 调 service(新终端)
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 - Client:
src/py_srv/py_srv/add_two_ints_client.py - launch:
src/bringup/launch/service_launch.py - 测试:
src/py_srv/test/test_srv.py
11.4 端到端日志
12. 进阶:异步服务端 + 回调式 client
12.1 异步服务端
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
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 是 Service demo;py_action_demo 是 Action demo。
接下来读
| 主题 | 文档 |
|---|---|
| Topic 深度 | 20-topics.md |
| Action 深度 | 40-actions.md |
| TF2 坐标变换 | 50-tf2.md |
| 三机部署 | 100-embedded-deployment.md |
📖 阅读路径导航
💡 这是仓库
doc/下所有文档的推荐阅读顺序。返回 README 总导航⏱ 本文预计阅读时间: 50 分钟 📍 当前位置: 第 14 / 24 篇
- ⏮ 上一篇: DDS 配置 + colcon overlay
- ⏭ 下一篇: Action 深度