Files
ROS2_learn/doc/30-services.md
T
2026-08-05 18:17:25 +08:00

13 KiB

30 · Service 深度:同步 req/resp(完全指南)

目标:吃透 ROS2 Service,涵盖 .srv 定义、Server/Client API、Future、wait_for_service、调试,学完能写工业级 Service。


目录


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):
        # 节点名(实际本仓库无 _py 后缀,launch 也不重命名)
        super().__init__('add_two_ints_server')
        # 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')
        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_intstake_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 源码

11.4 端到端日志

docker/srv_e2e.log


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 篇