py_srv — Python Service 请求-响应
ROS2 三种通信模式第二种:Service(服务)。同步、一问一答。
预计学习时间:1-2 小时。
前置知识:读完
py_pubsub的 README(知道节点和话题是什么)。可选:看完cpp_pubsub(Service Client/Server 也有 C++ 版,但跟 Python 几乎一一对应)。
这是什么?
Service 是 ROS2 的"打电话"模式:
- Client 发请求(Request),等回应
- Server 收到请求,处理完返回响应(Response)
- 同步的(等结果才能干别的)
对比三种通信模式:
| 模式 | 用途 | 是否同步 | 是否可取消 |
|---|---|---|---|
| Topic | 广播消息(传感器数据流) | ❌ 异步 | ❌ |
| Service | 一次性"问-答"(算数学、查数据) | ✅ 同步 | ❌ |
| Action | 长任务(导航、机械臂运动) | ❌ 异步 | ✅ |
生活化例子:
- Topic = 广播体操,大家都能听到
- Service = 打电话问"12+30=?",对方必须回答
- Action = 点外卖,可以看到骑手位置 + 可以取消订单
🎯 学完之后你能做什么?
- ✅ 写 Service Server(同步处理请求)
- ✅ 写 Service Client(用
call_async异步发 + spin 等结果) - ✅ 用
rclpy.spin_until_future_complete等待异步结果 - ✅ 手动用
ros2 service call测试服务 - ✅ 理解为什么不能在 callback 里阻塞
📁 文件结构
src/py_srv/
├── py_srv/
│ ├── add_two_ints_server.py # 服务端(a + b = sum)
│ └── add_two_ints_client.py # 客户端
├── launch/srv_launch.py # 一键启动 server + client
├── test/
│ ├── conftest.py
│ ├── test_srv_server.py # 服务端单元测试
│ ├── test_srv_client.py # 客户端单元测试
│ └── test_srv_roundtrip.py # 端到端测试
└── setup.py
关键服务类型:example_interfaces/srv/AddTwoInts(ROS2 自带的标准接口)
# srv 文件格式(本包用现成的)
int64 a
int64 b
---
int64 sum
--- 上面是 Request,下面是 Response。
🚀 跑起来
启动 Server
终端 1(容器内):
source /opt/ros/humble/setup.bash
source /root/ros2_ws/install/setup.bash
ros2 run py_srv add_two_ints_server
预期输出:
[INFO] [add_two_ints_server]: AddTwoIntsServer ready: service="add_two_ints"
启动 Client(另开终端)
终端 2:
source /opt/ros/humble/setup.bash
source /root/ros2_ws/install/setup.bash
ros2 run py_srv add_two_ints_client a:=12 b:=30
预期输出:
[INFO] [add_two_ints_client]: result: 12 + 30 = 42
手动调用服务(不开 Client)
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 12, b: 30}"
预期输出:
response: example_interfaces.srv.AddTwoInts_Response(sum=42)
📖 Server 代码
class AddTwoIntsServer(Node):
def __init__(self):
super().__init__('add_two_ints_server')
# create_service(消息类型, 服务名, 回调)
self.service_ = self.create_service(
AddTwoInts, 'add_two_ints', self._handle_request)
def _handle_request(self, request, response):
"""处理请求的回调。
注意签名: (request, response) -> response
必须原地修改 response,最后返回它。
"""
try:
response.sum = request.a + request.b # 处理逻辑
self.get_logger().info(
f'{request.a} + {request.b} = {response.sum}')
except Exception as exc:
self.get_logger().error(f'处理失败: {exc}')
response.sum = 0
return response # 必须返回
回调签名:(request, response) -> response,原地修改 response 然后返回。
📖 Client 代码(异步模式)
class AddTwoIntsClient(Node):
def __init__(self):
super().__init__('add_two_ints_client')
# create_client(消息类型, 服务名)
self._client = self.create_client(AddTwoInts, 'add_two_ints')
def call_once(self, a, b, timeout_sec=5.0):
"""异步调用 + spin 等结果"""
# 1) 等服务可用(必须轮询!)
if not self.wait_for_service_ready(timeout_sec):
self.get_logger().warn('service not available')
return None
# 2) 构造请求
request = AddTwoInts.Request()
request.a = a
request.b = b
# 3) 异步发请求(立刻返回,不等结果)
future = self._client.call_async(request)
# 4) spin 直到 future 完成(或超时)
rclpy.spin_until_future_complete(self, future, timeout_sec=timeout_sec)
# 5) 拿结果
if not future.done():
self.get_logger().warn('调用超时')
return None
return future.result().sum
call_async 特点:发完请求立刻返回 future,你可以干别的事,最后再 spin_until_future_complete 阻塞等结果。
wait_for_service_ready 轮询模式:
def wait_for_service_ready(self, timeout_sec=5.0):
deadline = time.time() + timeout_sec
while time.time() < deadline:
if self._client.service_is_ready():
return True
time.sleep(0.05) # 让出 CPU,下次再查
return False
⚠️ 为什么不能在 __init__ / callback 里阻塞等待?
spin 是单线程的:它需要反复执行 callback 才能让系统工作。如果在 callback 里阻塞,spin 就被卡住,其他 callback 全都执行不了。
真实场景的"死锁":
# ❌ 错误写法
class BadClient(Node):
def _on_timer(self):
# 这是订阅者回调(在 spin 里执行)
result = self._client.call(request) # ❌ 阻塞等结果
# call 内部要 spin,才能把 response callback 跑起来
# 但我们正在 spin 里 → 永远等不到 response callback 完成
# → 死锁!节点卡死
正确写法:异步(把响应处理放到 callback,而不是当前函数里阻塞等):
# ✅ 正确写法
class GoodClient(Node):
def _on_timer(self):
future = self._client.call_async(request)
# 立刻返回,不等结果
future.add_done_callback(self._response_callback)
def _response_callback(self, future):
# 这个 callback 由 spin 在将来某个时刻调用
result = future.result()
# ...处理 result
规律:任何在 callback / __init__ / spin 里的代码,都不能"同步等一个需要 spin 才会发生的事"。
🧪 跑测试
colcon test --packages-select py_srv
colcon test-result --all --verbose
预期:py_srv: pytest 7/7 ✓ 全部通过。
测试覆盖:
- 服务端:节点名 / 服务名 / 处理正负零(3 个)
- 客户端:能调用服务(1 个)
- 端到端:同进程跑 server + client 互调(2 个)
🔧 自己写 Service
-
定义 .srv 文件:
# srv/MyService.srv string name int32 count --- bool success string message(放在
srv/目录下) -
在
setup.py加一行(data_files):data_files=[ (...), (os.path.join('share', PACKAGE_NAME, 'srv'), glob('srv/*.srv')), ] -
重编译:
colcon build -
用:
from my_pkg.srv import MyService
📚 深入学习
- doc/30-services.md — Service 深度(异步 vs 同步、回调组)
⏭️ 下一个包
学完这个,继续学 py_action_demo — 学习 Action(带进度回调 + 可取消的长任务)。
📍 学习路径导航
| ⏮ 上一个 | 🏠 当前位置 | ⏭ 下一个 |
|---|---|---|
| py_pubsub — Python Topic | py_srv — Python Service | py_action_demo — Python Action |
📍 完整 12 包学习顺序见 主 README