# ROS2基础教程 - 5 服务通信

# 5.1 服务简介

服务通信也是ROS中一种极其常用的通信模式,但是和话题有很多不同。服务有点类似于方法的调用,客户端调用服务端,服务端返回结果给客户端。

服务分为服务端客户端

  • 服务端:提供服务的节点,等待其他节点发出请求,并根据请求进行相应的处理,然后返回结果。
  • 客户端:发起请求的节点,将请求发送给服务端,等待服务端处理并返回响应。

每个服务都需要定义一个服务类型,它包含请求响应的消息结构。服务的类型定义通常包含两个部分:

  • 请求:客户端发送给服务端的内容。
  • 响应:服务端返回给客户端的结果。

在 ROS 中,服务类型定义使用 .srv 文件,该文件包含请求响应两部分。典型的 .srv 文件分为两部分,中间通过 --- 分隔。

举个栗子:

# Request
int64 a
int64 b
---
# Response
int64 sum
1
2
3
4
5
6

在这个例子中,客户端请求两个整数的和,服务端返回求和结果。


服务和话题的区别:

特性 服务(Service) 话题(Topic)
通信模型 请求-响应,类似函数调用 发布-订阅,异步数据流
典型场景 一次性任务,如执行动作、路径规划等 持续数据流,如传感器数据、状态更新
消息传递方式 同步,双向通信,一发一收 异步,单向通信,一发可多收
消息持续性 一次性通信,发出请求得到响应后结束 持续发布与接收消息
实现难易度 简单,类似函数调用 复杂,适合实时性要求高的数据流
实时性 一般,服务需要等待请求处理完毕才能响应 高,适合实时性要求较高的场景
可靠性 可靠,请求必有响应 依赖QOS配置,可能丢失消息

使用建议:

  • 服务:适用于需要执行某个明确的、有限时长的任务,并且需要返回结果的场景,如远程调用某个动作或获取某个特定信息。
  • 话题:适用于持续数据流动的场景,如定期传递传感器数据、机器人位置信息等,不需要明确的请求和响应。

下面就通过服务来实现客户端请求服务端,传递两个整数,计算两个整数的和返回给客户端。

# 5.2 自定义通信接口

自定义通信接口,就是定义数据传输的格式。这里是服务通信接口,所以使用 .srv 文件。这个文件定义了服务请求和响应的数据格式。

首先在 工作空间/src 创建一个功能包,我这里叫 foooor_interface,我们定义话题、服务和以后动作的接口,都可以放在这个功能包下:

# 这里使用C++的方式
ros2 pkg create --build-type ament_cmake foooor_interface
1
2

因为 srv 通信接口文件需要使用 cmake 进行编译,所以使用 C++ 定义的功能包,要方便一些。


# 1 创建srv文件

首先在功能包根目录下创建一个 srv 目录,在 srv 目录下创建 AddTwoInts.srv 文件,内容如下:

# AddTwoInts.srv
int64 a
int64 b
---
int64 sum
1
2
3
4
5
  • 内容包括两个部分,请求部分和响应部分,中间使用 --- 分隔;

  • int64 aint64 b 是客户端请求的两个整数。

  • int64 sum 是服务端返回的结果(即两个整数的和)。

数据的类型在下一个章节再介绍。

# 2 配置srv文件

使用 srv ,需要在 CMakeLists.txtpackage.xml 进行一些配置。

修改 package.xml,在 package.xml 中,添加以下依赖项:

<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
1
2
3

这些依赖允许 ROS2 在构建时生成对应的服务代码。


CMakeLists.txt 文件中,添加对服务文件的依赖。

首先,确保你已经添加了 ament_cmakerosidl_default_generators 作为构建依赖项:

find_package(rosidl_default_generators REQUIRED)

rosidl_generate_interfaces(${PROJECT_NAME}
  "srv/AddTwoInts.srv"
  
  # 如果有多个srv或msg文件,在这里继续配置
  
  # DEPENDENCIES action_msgs std_msgs   # 如果用到其他依赖,可以在这里配置
)
1
2
3
4
5
6
7
8
9

这个时候可以执行 colcon build 来构建项目,会根据 srv 文件生成供的 Python 和 C++ 调用的数据结构。

# 5.3 Python实现服务通信

首先在 工作空间/src 创建一个功能包,我这里叫 service_python

ros2 pkg create --build-type ament_python service_python
1

然后下面在 service_python/service_python 创建两个节点,一个是服务端,一个是客户端。


# 1 引入srv通信接口

通信接口是定义在 foooor_interface 功能包下的,所以需要在当前功能包中引入 foooor_interface 功能包:

在功能包的 package.xml 中,添加如下配置:

<depend>foooor_interface</depend>
1

导入依赖是为了能够让我们的代码找到对应的通信接口。

# 2 创建服务端

创建服务端节点文件,例如 calculate_server.py,内容如下:

import rclpy
from rclpy.node import Node

# 导入我们定义的服务类型 AddTwoInts,它是从 foooor_interface.srv 中引入的
from foooor_interface.srv import AddTwoInts

# 创建一个继承自 Node 的类,用于实现服务端节点
class AddTwoIntsServer(Node):
    def __init__(self):
        super().__init__('add_two_ints_server')
        
        # 创建一个服务,服务名为 'add_two_ints',类型为 AddTwoInts,回调函数为 add_two_ints_callback(接收请求会调用)
        self.service = self.create_service(AddTwoInts, 'add_two_ints', self.add_two_ints_callback)

    # 定义服务的回调函数,处理客户端的请求
    def add_two_ints_callback(self, request, response):
        # 从客户端请求中获取两个整数 a 和 b,并将它们相加
        response.sum = request.a + request.b
        
        # 在日志中输出接收到的请求和计算结果
        self.get_logger().info(f'Request: {request.a} + {request.b} = {response.sum}')
        
        # 返回包含计算结果的响应对象
        return response

# 主函数,ROS2 Python 节点的入口
def main(args=None):
    # 初始化 ROS2 Python 客户端库
    rclpy.init(args=args)
    # 创建并初始化 AddTwoIntsServer 节点
    node = AddTwoIntsServer()
    
    # 开始处理 ROS2 通信(服务请求),等待并处理回调函数
    rclpy.spin(node)
    
    # 销毁节点
    node.destroy_node()
    # 关闭并清理 ROS2 系统
    rclpy.shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39

在上面的代码中,创建了一个服务,指定了服务的类型、名称和回调方法,收到请求后会调用回调方法。回调方法有两个参数,从request 中获取参数,将结果放到 response 中,返回 response。

# 3 创建客户端

创建客户端节点文件,例如 calculate_client.py,内容如下:

# 导入 ROS2 所需的模块
import rclpy  # ROS2 Python客户端库
from rclpy.node import Node  # ROS2 节点类
from foooor_interface.srv import AddTwoInts  # 引入你定义的服务类型(AddTwoInts)
import sys  # 用于获取命令行参数

# 创建客户端类,继承自 ROS2 节点类
class AddTwoIntsClient(Node):
    def __init__(self):
        # 调用父类 Node 的初始化方法,创建节点并命名为 'add_two_ints_client'
        super().__init__('add_two_ints_client')
        # 创建客户端,服务类型为 AddTwoInts,服务名为 'add_two_ints'
        self.client = self.create_client(AddTwoInts, 'add_two_ints')
        # 等待服务端上线,超时时间为 1 秒,若超时则继续等待
        while not self.client.wait_for_service(timeout_sec=1.0):
            # 输出日志,提示等待服务端
            self.get_logger().info('Waiting for service...')
        # 创建一个服务请求实例
        self.request = AddTwoInts.Request()
    
    # 发送服务请求
    def send_request(self, a, b):
        # 设置请求中的两个整数值,使用从命令行获取的参数
        self.request.a = a
        self.request.b = b
        # 发送异步请求,并返回 future 对象(代表异步操作的结果)
        future = self.client.call_async(self.request)
        return future

# 客户端节点的主入口
def main(args=None):
    # 初始化 ROS2 客户端库
    rclpy.init(args=args)

    # 从命令行参数获取输入的两个整数
    if len(sys.argv) != 3:  # 检查命令行参数数量是否正确
        node.get_logger().error("Usage: add_two_ints_client.py a b")
        return

    a = int(sys.argv[1])  # 第一个整数参数
    b = int(sys.argv[2])  # 第二个整数参数

    # 创建客户端节点实例
    node = AddTwoIntsClient()
    # 发送请求并获取 future 对象
    future = node.send_request(a, b)

    # 等待服务端返回结果
    rclpy.spin_until_future_complete(node, future)
    # 检查请求是否成功
    if future.result() is not None:
        # 如果有结果,输出相加后的结果
        node.get_logger().info(f'Result: {future.result().sum}')
    else:
        # 如果请求失败,输出日志
        node.get_logger().info('Service call failed')
    
    # 销毁节点
    node.destroy_node()
    # 关闭 ROS2 客户端库
    rclpy.shutdown()
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61

上面创建了一个客户端节点,首先创建了一个调用服务的客户端,指定了服务类型和名称,因为服务端可能没启动,所以这里等待服务上线,等待超时时间为1秒,超过1秒会继续下一次等待,最后调用 call_async() 异步调用服务,你也可以调用 call() 方法同步调用。

请求的参数是从终端获取的,也就是运行节点的时候,在命令后添加参数,然后这里获取到参数。

# 4 配置节点

在功能包下的 setup.py 中配置两个节点,在 entry_points 中配置:

entry_points={
    'console_scripts': [
        'calculate_server = service_python.calculate_server:main',
        'calculate_client= service_python.calculate_client:main',
    ],
},
1
2
3
4
5
6

# 5 构建项目

现在代码已经编写完成了,需要构建项目。

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

# 6 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


然后打开一个终端,启动服务端节点:

ros2 run service_python calculate_server
1

此时服务端已经在等待调用了。


打开另一个终端,运行客户端节点:

ros2 run service_python calculate_client 3 5
1

后面的 35 是请求的参数,可以看到,客户端调用成功,如下:

服务端此时也可以看到收到请求的日志:

其实也可以先运行客户端节点,只是这个时候服务端节点还未启动,客户端节点一直在等待。

# 5.4 C++实现服务通信

首先在 工作空间/src 创建一个功能包,我这里叫 service_cpp

ros2 pkg create --build-type ament_cmake service_cpp
1

然后下面在 service_cpp/src 下创建两个节点,一个是发布消息的节点,一个是订阅消息的节点。


# 1 引入srv通信接口

通信接口是定义在 foooor_interface 功能包下的,所以需要在当前功能包中引入 foooor_interface 功能包:

在功能包的 package.xml 中,添加如下配置:

<depend>foooor_interface</depend>
1

导入依赖是为了能够让我们的代码找到对应的通信接口。

# 2 创建服务端

创建节点文件 calculate_server.cpp,内容如下:

#include <rclcpp/rclcpp.hpp>
#include "foooor_interface/srv/add_two_ints.hpp"  // 注意这里是生成的接口

using AddTwoInts = foooor_interface::srv::AddTwoInts;  // 简化类型名

class AddTwoIntsServer : public rclcpp::Node {
public:
  AddTwoIntsServer() : Node("add_two_ints_server") {
    // 创建服务,并指定回调函数处理请求
    service_ = this->create_service<AddTwoInts>(
        "add_two_ints", std::bind(&AddTwoIntsServer::handle_service, this, std::placeholders::_1, std::placeholders::_2));
    RCLCPP_INFO(this->get_logger(), "Service is ready: add_two_ints");
  }

private:
  void handle_service(const std::shared_ptr<AddTwoInts::Request> request,
                      std::shared_ptr<AddTwoInts::Response> response) {
    // 处理请求:计算 a + b
    response->sum = request->a + request->b;
    RCLCPP_INFO(this->get_logger(), "Request: a = %ld, b = %ld, responding with sum = %ld",
                request->a, request->b, response->sum);
  }

  rclcpp::Service<AddTwoInts>::SharedPtr service_;
};

int main(int argc, char **argv) {
  // 初始化 rclcpp
  rclcpp::init(argc, argv);

  // 创建服务节点并运行
  auto node = std::make_shared<AddTwoIntsServer>();
  rclcpp::spin(node);

  // 销毁节点并清理资源
  rclcpp::shutdown();
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

在上面的代码中,创建了一个服务,指定了服务的类型、名称和回调方法,收到请求后会调用回调方法。回调方法有两个参数,从request 中获取参数,将结果放到 response 中,返回 response。

std::placeholders::_1std::placeholders::_2 是占位符,它们分别对应服务处理函数的第一个参数(请求对象)和第二个参数(响应对象),_1_2 的值在实际调用时会由 ROS2 自动传入,分别代表服务的请求和响应。

# 3 创建客户端

创建节点文件 calculate_client.cpp,内容如下:

#include <rclcpp/rclcpp.hpp>
#include "foooor_interface/srv/add_two_ints.hpp"  // 注意这里是生成的接口
#include <chrono>
#include <cstdlib>
#include <memory>

using namespace std::chrono_literals;
using AddTwoInts = foooor_interface::srv::AddTwoInts;  // 简化类型名

class AddTwoIntsClient : public rclcpp::Node {
public:
  AddTwoIntsClient() : Node("add_two_ints_client") {
    // 创建客户端
    client_ = this->create_client<AddTwoInts>("add_two_ints");

    // 等待服务可用
    while (!client_->wait_for_service(1s)) {
      RCLCPP_INFO(this->get_logger(), "Waiting for service to appear...");
    }
  }

  // 发送请求并等待响应
  void send_request(int64_t a, int64_t b) {
    auto request = std::make_shared<AddTwoInts::Request>();
    request->a = a;
    request->b = b;

    // 异步发送请求
    auto future = client_->async_send_request(request);

    // 等待响应
    if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) ==
        rclcpp::FutureReturnCode::SUCCESS) {
      RCLCPP_INFO(this->get_logger(), "Result: %ld", future.get()->sum);
    } else {
      RCLCPP_ERROR(this->get_logger(), "Failed to call service add_two_ints");
    }
  }

private:
  rclcpp::Client<AddTwoInts>::SharedPtr client_;
};

int main(int argc, char **argv) {
  // 初始化 rclcpp
  rclcpp::init(argc, argv);

  // 检查输入参数
  if (argc != 3) {
    RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "Usage: add_two_ints_client X Y");
    return 1;
  }

  // 创建客户端节点
  auto node = std::make_shared<AddTwoIntsClient>();

  // 发送请求,读取命令行参数,从命令行读取两个参数
  int64_t a = std::stoll(argv[1]);
  int64_t b = std::stoll(argv[2]);
  node->send_request(a, b);

  // 清理节点
  rclcpp::shutdown();
  return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65

上面创建了一个客户端节点,首先创建了一个调用服务的客户端,指定了服务类型和名称,因为服务端可能没启动,所以这里等待服务上线,等待超时时间为1秒,超过1秒会继续下一次等待。在请求的时候,首先创建请求对象,封装请求参数,最后调用 async_send_request() 异步调用服务,你也可以调用 send_request() 方法同步调用。

上面请求的参数是在启动节点的时候,在命令行后面添加的参数,从命令行获取的。

# 4 配置CMakeLists.txt

在功能包下的 CMakeLists.txt 中,添加节点配置,可以添加在 find_package(ament_cmake REQUIRED) 下面。

# 找到此包需要的其他包
find_package(rclcpp REQUIRED)
find_package(foooor_interface REQUIRED)

# 添加可执行文件,指向 calculate_server.cpp
add_executable(calculate_server src/calculate_server.cpp)
ament_target_dependencies(calculate_server rclcpp foooor_interface)  # 注意需要rclcpp、foooor_interface

# 添加可执行文件,指向 calculate_client.cpp
add_executable(calculate_client src/calculate_client.cpp)
ament_target_dependencies(calculate_client rclcpp foooor_interface)  # 注意需要rclcpp、foooor_interface

# 安装可执行文件
install(TARGETS
  calculate_server
  calculate_client
  DESTINATION lib/${PROJECT_NAME}
)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18

上面配置了 calculate_servercalculate_client 两个节点。

# 5 构建项目

现在代码已经编写完成了,需要构建项目。

工作空间下执行如下命令:

colcon build
1

构建完成,会在 install 目录下生成文件。

# 6 运行节点

首先执行 source 命令,在工作空间下执行:

source install/local_setup.sh
1

上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。


然后打开一个终端,启动服务端节点:

ros2 run service_cpp calculate_server
1

此时服务端已经在等待调用了。


打开另一个终端,运行客户端节点:

ros2 run service_cpp calculate_client 3 5
1

后面的 35 是请求的参数。

可以看到,客户端调用成功,和 Python 实现一样。