# ROS2基础教程 - 5 服务通信
# 5.1 服务简介
服务通信也是ROS中一种极其常用的通信模式,但是和话题有很多不同。服务有点类似于方法的调用,客户端调用服务端,服务端返回结果给客户端。
服务分为服务端和客户端:
- 服务端:提供服务的节点,等待其他节点发出请求,并根据请求进行相应的处理,然后返回结果。
- 客户端:发起请求的节点,将请求发送给服务端,等待服务端处理并返回响应。
每个服务都需要定义一个服务类型,它包含请求和响应的消息结构。服务的类型定义通常包含两个部分:
- 请求:客户端发送给服务端的内容。
- 响应:服务端返回给客户端的结果。
在 ROS 中,服务类型定义使用 .srv
文件,该文件包含请求和响应两部分。典型的 .srv
文件分为两部分,中间通过 ---
分隔。
举个栗子:
# Request
int64 a
int64 b
---
# Response
int64 sum
2
3
4
5
6
在这个例子中,客户端请求两个整数的和,服务端返回求和结果。
服务和话题的区别:
特性 | 服务(Service) | 话题(Topic) |
---|---|---|
通信模型 | 请求-响应,类似函数调用 | 发布-订阅,异步数据流 |
典型场景 | 一次性任务,如执行动作、路径规划等 | 持续数据流,如传感器数据、状态更新 |
消息传递方式 | 同步,双向通信,一发一收 | 异步,单向通信,一发可多收 |
消息持续性 | 一次性通信,发出请求得到响应后结束 | 持续发布与接收消息 |
实现难易度 | 简单,类似函数调用 | 复杂,适合实时性要求高的数据流 |
实时性 | 一般,服务需要等待请求处理完毕才能响应 | 高,适合实时性要求较高的场景 |
可靠性 | 可靠,请求必有响应 | 依赖QOS配置,可能丢失消息 |
使用建议:
- 服务:适用于需要执行某个明确的、有限时长的任务,并且需要返回结果的场景,如远程调用某个动作或获取某个特定信息。
- 话题:适用于持续数据流动的场景,如定期传递传感器数据、机器人位置信息等,不需要明确的请求和响应。
下面就通过服务来实现客户端请求服务端,传递两个整数,计算两个整数的和返回给客户端。
# 5.2 自定义通信接口
自定义通信接口,就是定义数据传输的格式。这里是服务通信接口,所以使用 .srv
文件。这个文件定义了服务请求和响应的数据格式。
首先在 工作空间/src
创建一个功能包,我这里叫 foooor_interface
,我们定义话题、服务和以后动作的接口,都可以放在这个功能包下:
# 这里使用C++的方式
ros2 pkg create --build-type ament_cmake foooor_interface
2
因为 srv 通信接口文件需要使用 cmake 进行编译,所以使用 C++ 定义的功能包,要方便一些。
# 1 创建srv文件
首先在功能包根目录下创建一个 srv
目录,在 srv
目录下创建 AddTwoInts.srv
文件,内容如下:
# AddTwoInts.srv
int64 a
int64 b
---
int64 sum
2
3
4
5
内容包括两个部分,请求部分和响应部分,中间使用
---
分隔;int64 a
和int64 b
是客户端请求的两个整数。int64 sum
是服务端返回的结果(即两个整数的和)。
数据的类型在下一个章节再介绍。
# 2 配置srv文件
使用 srv
,需要在 CMakeLists.txt
和 package.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>
2
3
这些依赖允许 ROS2 在构建时生成对应的服务代码。
在 CMakeLists.txt
文件中,添加对服务文件的依赖。
首先,确保你已经添加了 ament_cmake
和 rosidl_default_generators
作为构建依赖项:
find_package(rosidl_default_generators REQUIRED)
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/AddTwoInts.srv"
# 如果有多个srv或msg文件,在这里继续配置
# DEPENDENCIES action_msgs std_msgs # 如果用到其他依赖,可以在这里配置
)
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
然后下面在 service_python/service_python
创建两个节点,一个是服务端,一个是客户端。
# 1 引入srv通信接口
通信接口是定义在 foooor_interface
功能包下的,所以需要在当前功能包中引入 foooor_interface
功能包:
在功能包的 package.xml
中,添加如下配置:
<depend>foooor_interface</depend>
导入依赖是为了能够让我们的代码找到对应的通信接口。
# 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()
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()
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',
],
},
2
3
4
5
6
# 5 构建项目
现在代码已经编写完成了,需要构建项目。
在工作空间下执行如下命令:
colcon build
构建完成,会在 install
目录下生成文件。
# 6 运行节点
首先执行 source
命令,在工作空间下执行:
source install/local_setup.sh
上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。
然后打开一个终端,启动服务端节点:
ros2 run service_python calculate_server
此时服务端已经在等待调用了。
打开另一个终端,运行客户端节点:
ros2 run service_python calculate_client 3 5
后面的 3
和 5
是请求的参数,可以看到,客户端调用成功,如下:
服务端此时也可以看到收到请求的日志:
其实也可以先运行客户端节点,只是这个时候服务端节点还未启动,客户端节点一直在等待。
# 5.4 C++实现服务通信
首先在 工作空间/src
创建一个功能包,我这里叫 service_cpp
:
ros2 pkg create --build-type ament_cmake service_cpp
然后下面在 service_cpp/src
下创建两个节点,一个是发布消息的节点,一个是订阅消息的节点。
# 1 引入srv通信接口
通信接口是定义在 foooor_interface
功能包下的,所以需要在当前功能包中引入 foooor_interface
功能包:
在功能包的 package.xml
中,添加如下配置:
<depend>foooor_interface</depend>
导入依赖是为了能够让我们的代码找到对应的通信接口。
# 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;
}
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::_1
和 std::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;
}
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}
)
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
上面配置了 calculate_server
和 calculate_client
两个节点。
# 5 构建项目
现在代码已经编写完成了,需要构建项目。
在工作空间下执行如下命令:
colcon build
构建完成,会在 install
目录下生成文件。
# 6 运行节点
首先执行 source
命令,在工作空间下执行:
source install/local_setup.sh
上面的命令是让 ROS 找到我们的功能包,已经在 HelloWorld 章节说过了。
然后打开一个终端,启动服务端节点:
ros2 run service_cpp calculate_server
此时服务端已经在等待调用了。
打开另一个终端,运行客户端节点:
ros2 run service_cpp calculate_client 3 5
后面的 3
和 5
是请求的参数。
可以看到,客户端调用成功,和 Python 实现一样。