话题通信的数据传输是单向的,订阅端被动接收发布端的数据。这时候有人就问了,如果发布端想主动接收数据怎么办。ROS中提供了另一种通信方式:服务通信。
ROS:通信模型
Service通信是双向的,它不仅可以发送消息,同时还会有反馈。所以service包括两部分,一部分是请求方(Clinet),另一部分是应答方/服务提供方(Server)。这时请求方(Client)就会发送一个request,要等待server处理,反馈回一个reply,这样通过类似“请求-应答”的机制完成整个服务通信。
这种通信方式的示意图如下:
- Node B是server(应答方),提供了一个服务的接口,叫做/Service,我们一般都会用string类型来指定service的名称,类似于topic。
- Node A向Node B发起了请求,经过处理后得到了反馈。
Service是同步通信方式,所谓同步就是说,此时Node A发布请求后会在原地等待reply,直到Node B处理完了请求并且完成了reply,Node A才会继续执行。Node A等待过程中,是处于阻塞状态的成通信。这样的通信模型没有频繁的消息传递,没有冲突与高系统资源的占用,只有接受请求才执行服务,简单而且高效。
一般用于偶然的、临时的、对实时性有要求、有一定逻辑处理需求的数据传输场景。
ROS:topic VS service
名称 | Topic | service |
---|---|---|
通信方式 | 异步通信 | 同步通信 |
实现原理 | TCP/IP | TCP/IP |
通信模型 | Publish-Subscribe | Request-Reply |
映射关系 | Publish-Subscribe(多对多) | Request-Reply(多对一) |
特点 | 接受者收到数据会回调(Callback) | 远程过程调用(RPC)服务器端的服务 |
应用场景 | 连续、高频的数据发布 | 偶尔使用的功能/具体的任务 |
举例 | 激光雷达、里程计发布数据 | 开关传感器、拍照、逆解计算 |
注意:远程过程调用(Remote Procedure Call,RPC),可以简单通俗的理解为在一个进程里调用另一个进程的函数。
ROS:Srv是个什么鬼
类似msg文件,srv文件是用来描述服务(service数据类型的,service通信的数据格式定义在*.srv中。它声明了一个服务,包括请求(request)和响应(reply)两部分。
srv文件格式很固定,第一行是请求的格式,中间用—隔开,第三行是应答的格式。
举个例子:
- msgs_demo/srv/DetectHuman.srv
- 作用:用来查询当前深度摄像头中的人体姿态和关节数的
- 请求为是否开始检测
- 应答为一个数组:
- 数组的每个元素为某个人的姿态(HumanPose)。
- 而对于人的姿态,其实是一个msg,所以srv可以嵌套msg在其中,但它不能嵌套srv。
bool start_detect
---
my_pkg/HumanPose[] pose_data
- msgs_demo/msg/HumanPose.msg
std_msgs/Header header
string uuid
int32 number_of_joints
my_pkg/JointPose[]joint_data
- msgs_demo/msg/JointPose.msg
string joint_name
geometry_msgs/Pose pose
floar32 confidence
定义完了msg、srv文件,还有重要的一个步骤就是修改package.xml和修改CMakeList.txt。这些文件需要添加一些必要的依赖等,例如:
<build_depend>** message_generation **</build_depend>
<run_depend>** message_runtime **</run_depend>
上述文本中“**”所引就是新添加的依赖。又例如:
find_package(...roscpp rospy std_msgs ** message_generation **)
catkin_package(
...
CATJIN_DEPENDS ** message_runtime ** ...
...)
add_message_file(
FILES
** DetectHuman.srv **
** HumanPose.msg **
** JointPos.msg **)
** generate_messages(DEPENDENCIES std_msgs) **
ROS:常用命令
ROS1:service命令 | 作用 |
---|---|
rosservice list | 查看服务列表 |
rosservice info | 打印服务信息 |
rosservice type | 打印服务类型 |
rosservice uri | 打印服务ROSRPC uri |
rosservice find | 按服务类型查找服务 |
rosservice call | 使用所提供的args调用服务 |
rosservice args | 打印服务参数 |
ROS1:srv命令 | 作用 |
rossrv show | 显示服务描述 |
rossrv list | 列出所有服务 |
rossrv md5 | 显示服务md5sum |
rossrv package | 列出包中的服务 |
rossrv packages | 列出包含服务的包 |
ROS2:service命令 | 作用 |
ros2 service list | 查看服务列表 |
ros2 service find example_interfaces/srv/AddTwoInts | 查找使用某一接口的服务 |
ros2 service type /add_two_ints | 查看服务接口类型: |
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts “{a: 5,b: 10}” | 手动调用服务(有参) |
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts | 手动调用服务(无参) |
ROS2:体验一下
(1)启动服务端
打开终端,运行下面的命令,这个命令用于运行一个服务节点,这个服务的功能是将两个数字相加,给定a,b两个数,返回sum也就是ab之和。
ros2 run examples_rclpy_minimal_service service
(2)使用命令查看服务列表
ros2 service list
(3)手动调用服务
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"
ROS: 建立流程
三个角色:
- client(客户端):负责向服务端发送请求,并接收服务端发送的数据。
- server(服务端):负责接收处理请求,并对客户端做出响应。
- ROS Master(管理者):负责保管客户端和服务端注册的信息,并匹配话题相同的客户端与服务端,帮助客户端与服务端建立连接。
流程如下:
- step0:服务端启动,在管理者处注册,其中包含提供的服务的名称。管理者会将节点的注册信息加入到注册表中。
- step1:客户端启动,在管理者处注册,其中包含请求的服务的名称。管理者会将节点的注册信息加入到注册表中。
- step2:管理者根据服务端和客户端的服务名称进行匹配,并将服务端的地址信息告知客户端。
- step3:客户端与服务端进行网络连接,并发送有关请求。
- step4:服务端处理请求数据,产生响应并发送给客户端。
注意:
- 客户端请求被处理时,要保证服务端已经启动
- 服务端和客户端都可以存在多个
ROS2:代码实现
RCLCPP实现
创建功能包和节点
cd chapt3/chapt3_ws/src
ros2 pkg create example_service_rclcpp --build-type ament_cmake --dependencies rclcpp
touch example_service_rclcpp/src/service_server_01.cpp
touch example_service_rclcpp/src/service_client_01.cpp
- service_server_01.cpp
#include "rclcpp/rclcpp.hpp"
class ServiceServer01 : public rclcpp::Node {
public:
ServiceServer01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
}
private:
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
auto node = std::make_shared<ServiceServer01>("service_server_01");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
- service_client_01.cpp
#include "rclcpp/rclcpp.hpp"
class ServiceClient01 : public rclcpp::Node {
public:
// 构造函数,有一个参数为节点名称
ServiceClient01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
}
private:
};
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
/*产生一个的节点*/
auto node = std::make_shared<ServiceClient01>("service_client_01");
/* 运行节点,并检测退出信号*/
node->send_request(5, 6);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
- CMakeLists.txt
add_executable(service_client_01 src/service_client_01.cpp)
ament_target_dependencies(service_client_01 rclcpp)
add_executable(service_server_01 src/service_server_01.cpp)
ament_target_dependencies(service_server_01 rclcpp)
install(TARGETS
service_server_01
DESTINATION lib/${PROJECT_NAME}
)
install(TARGETS
service_client_01
DESTINATION lib/${PROJECT_NAME}
)
- 编译测试
cd chapt3/chapt3_ws/
colcon build --packages-select example_service_rclcpp
# 运行 service_server_01
source install/setup.bash
ros2 run example_service_rclcpp service_server_01
# 打开新终端运行 service_client_01
source install/setup.bash
ros2 run example_service_rclcpp service_client_01
服务端实现
导入接口
ament_cmake类型功能包导入消息接口分为三步:
- 在CMakeLists.txt中导入,具体是先find_packages再ament_target_dependencies
# 这里我们一次性把服务端和客户端对example_interfaces的依赖都加上
find_package(example_interfaces REQUIRED)
add_executable(service_client_01 src/service_client_01.cpp)
ament_target_dependencies(service_client_01 rclcpp example_interfaces)
add_executable(service_server_01 src/service_server_01.cpp)
ament_target_dependencies(service_server_01 rclcpp example_interfaces)
- 在packages.xml中导入,具体是添加depend标签并将消息接口写入。
<depend>example_interfaces</depend>
- 在代码中导入,C++中是#include"消息功能包/xxx/xxx.hpp"。
#include "example_interfaces/srv/add_two_ints.hpp"
编写代码
- create_service,参考rclcpp API文档即可
- ServiceT,消息接口example_interfaces::srv::AddTwoInts
- service_name,服务名称
- callback,回调函数,使用成员函数作为回调函数,std::bind进行转换
- qos_profile,服务质量配置文件,默认rmw_qos_profile_services_default
- group,调用服务的回调组,默认nullptr
#include "example_interfaces/srv/add_two_ints.hpp"
#include "rclcpp/rclcpp.hpp"
class ServiceServer01 : public rclcpp::Node {
public:
ServiceServer01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
// 创建服务
add_ints_server_ =
this->create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints_srv",
std::bind(&ServiceServer01::handle_add_two_ints, this,
std::placeholders::_1, std::placeholders::_2));
}
private:
// 声明一个服务
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr
add_ints_server_;
// 收到请求的处理函数
void handle_add_two_ints(
const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response) {
RCLCPP_INFO(this->get_logger(), "收到a: %ld b: %ld", request->a,
request->b);
response->sum = request->a + request->b;
};
};
测试
cd chapt3_ws/
colcon build --packages-select example_service_rclcpp
source install/setup.bash
ros2 run example_service_rclcpp service_server_01
接着打开一个新的终端
# 你应该可以看到我们声明的服务
ros2 service list
# 使用命令行进行调用
ros2 service call /add_two_ints_srv example_interfaces/srv/AddTwoInts "{a: 5,b: 10}"
客户端实现
编写代码
写代码之前要先看api文档
- create_client
- 发送请求:async_send_request
- request,请求的消息,这里用于放a,b两个数。
- CallBack,回调函数,异步接收服务器的返回的函数
- wait_for_service:这个函数是用于等待服务上线的,这个函数并不在rclcpp::Client中定义,而是在其父类中定义的。
- 参数就一个,等待的时间
- 返回值是bool类型的,上线了就是true,不上线就是false。
#include "example_interfaces/srv/add_two_ints.hpp"
class ServiceClient01 : public rclcpp::Node {
public:
// 构造函数,有一个参数为节点名称
ServiceClient01(std::string name) : Node(name) {
RCLCPP_INFO(this->get_logger(), "节点已启动:%s.", name.c_str());
// 创建客户端
client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints_srv");
}
void send_request(int a, int b) {
RCLCPP_INFO(this->get_logger(), "计算%d+%d", a, b);
// 1.等待服务端上线
while (!client_->wait_for_service(std::chrono::seconds(1))) {
//等待时检测rclcpp的状态
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "等待服务的过程中被打断...");
return;
}
RCLCPP_INFO(this->get_logger(), "等待服务端上线中");
}
// 2.构造请求的
auto request =
std::make_shared<example_interfaces::srv::AddTwoInts_Request>();
request->a = a;
request->b = b;
// 3.发送异步请求,然后等待返回,返回时调用回调函数
client_->async_send_request(
request, std::bind(&ServiceClient01::result_callback_, this,
std::placeholders::_1));
};
private:
// 声明客户端
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
void result_callback_(
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedFuture
result_future) {
auto response = result_future.get();
RCLCPP_INFO(this->get_logger(), "计算结果:%ld", response->sum);
}
};
测试
最后还要修改下主函数,用于调用服务端发送请求
int main(int argc, char** argv) {
rclcpp::init(argc, argv);
/*产生一个的节点*/
auto node = std::make_shared<ServiceClient01>("service_client_01");
/* 运行节点,并检测退出信号*/
//增加这一行,node->send_request(5, 6);,计算5+6结果
node->send_request(5, 6);
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
- 编译运行客户端:
cd chapt3_ws/
colcon build --packages-select example_service_rclcpp
source install/setup.bash
ros2 run example_service_rclcpp service_client_01
- 打开服务端,让服务上线:
source install/setup.bash
ros2 run example_service_rclcpp service_server_01