系列文章目录
留空
前言
自用
一、题目
前面的“话题”中,李四创建了一个“小说”话题,并在上面发布和更新“没空”小说的章节,王二订阅此话题。为了把“话题”和“服务”的代码分开,我们再加入王五,王五和王二一样,都订阅了“小说”话题。
然后,张三也想看“没空”小说,但是不想一章一章的看,所以支付给王五钱,请求王五小说每更新五章,就一块发给他(请求)。王五同意了并等待李四更新小说,凑够五章就给张三发过去(响应)。
所以,这次我们主要任务就是
-
编写王五服务端:等待李四更新五章小说,凑够五章给张三发过去。
-
编写张三客户端:等待王五凑够五章,付钱之后收到五章小说。
二、创建功能包和节点
(1)创建功能包
cd towm_ws/src
ros2 pkg create learning_service --build-type ament_cmake --dependencies rclcpp
(2)创建节点
三、使用RCLCPP编写话题
1.基础代码
(1)张三客户端 zhang3.cpp
#include "rclcpp/rclcpp.hpp"
class BuyServer : public rclcpp::Node
{
public:
BuyServer(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"买书客户端%s已启动",name.c_str());
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<BuyServer>("zhang3");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
(2)王五服务端 wang5.cpp
(直接复制wang2.cpp
)
或者可以看一下这篇:一个节点实现两个功能
#include "rclcpp/rclcpp.hpp"
//(1)导入订阅的话题接口类型
#include "std_msgs/msg/string.hpp"
class ReadersAndSellServer : public rclcpp::Node
{
public:
ReadersAndSellServer(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"读者节点%s已启动",name.c_str());
RCLCPP_INFO(this->get_logger(),"卖书服务端%s已启动",name.c_str());
//(3)创建订阅者
sub_readers = this->create_subscription<std_msgs::msg::String>
("no_time",10,std::bind(&ReadersAndSellServer::readnovels_callback,this,std::placeholders::_1));
}
private:
//(2)声明订阅者
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_readers;
//(4)创建订阅回调函数
void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
{
//(5)编写订阅回调函数
RCLCPP_INFO(this->get_logger(),"王五已阅读%s",novels->data.c_str());
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<ReadersAndSellServer>("wang5");
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
(3)每次编写完代码记得修改CmakeLists.txt
,添加以下代码
注: wang5_node
可执行文件依赖于rclcpp
和std_msgs
两个软件包
find_package(std_msgs REQUIRED)
add_executable(wang5_node src/wang5.cpp)
ament_target_dependencies(wang5_node rclcpp std_msgs)
add_executable(zhang3_node src/zhang3.cpp)
ament_target_dependencies(zhang3_node rclcpp)
install(TARGETS
wang5_node
zhang3_node
DESTINATION lib/${PROJECT_NAME}
)
还要修改package.xml
,添加第二行代码。
<depend>rclcpp</depend>
<depend>std_msgs</depend>
(4)测试
打开一个终端
cd towm_ws
colcon build --packages-select learning_service
source install/setup.sh
ros2 run learning_service wang5_node
打开另一个终端
cd towm_ws
source install/setup.sh
ros2 run learning_service zhang3_node
再打开另一个终端
cd towm_ws
colcon build --packages-select learning_topic
source install/setup.sh
ros2 run learning_topic li4_node
若无报错,我们就可以开始编写服务端和客户端的代码了。
2.编写王五服务端代码(卖书)
完整代码
#include "rclcpp/rclcpp.hpp"
//(1)导入订阅的话题接口类型
#include "std_msgs/msg/string.hpp"
//1.导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
// 引入C++标准库中的队列容器模板(先进先出(FIFO)的数据结构)
#include <queue>
class ReadersAndSellServer : public rclcpp::Node
{
public:
ReadersAndSellServer(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"读者节点%s已启动",name.c_str());
RCLCPP_INFO(this->get_logger(),"卖书服务端%s已启动",name.c_str());
//(3)创建订阅者
sub_readers = this->create_subscription<std_msgs::msg::String>
("no_time",10,std::bind(&ReadersAndSellServer::readnovels_callback,this,std::placeholders::_1));
// 5、创建一个服务回调组
callback_group_service = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
// 4、创建服务端
sellserver = this->create_service<learning_interfaces1::srv::SellNovel>
("sell_novels",std::bind(&ReadersAndSellServer::SellNovel_callback,this,std::placeholders::_1,std::placeholders::_2),
rmw_qos_profile_default,callback_group_service);
}
private:
//(2)声明订阅者
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_readers;
// 3、声明服务端
rclcpp::Service<learning_interfaces1::srv::SellNovel>::SharedPtr sellserver;
// 4、声明一个服务回调组
rclcpp::CallbackGroup::SharedPtr callback_group_service;
// 创建一个小说章节队列(书库)
std::queue<std::string> novels_queue;
//(4)创建订阅回调函数
void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
{
//(5)编写订阅回调函数
RCLCPP_INFO(this->get_logger(),"王五已阅读%s",novels->data.c_str());
//把收到的小说存入书库
novels_queue.push(novels->data);
}
// 2、创建服务端回调函数
void SellNovel_callback(const learning_interfaces1::srv::SellNovel::Request::SharedPtr request,
const learning_interfaces1::srv::SellNovel::Response::SharedPtr response)
{
//第一步:判断张三给的钱数是否足够(每章1元)
unsigned int sendnovel_flag = 0; //是否发送小说标志位
unsigned int buymoney = request -> money; //张三发来的买书钱
if(buymoney == 5)
{
sendnovel_flag = 1;
RCLCPP_INFO(this->get_logger(),"已收到张三%d元",buymoney);
}
else
{
sendnovel_flag = 0;
}
//第二步:判断小说章节是否足够五章
if(novels_queue.size() < 5)
{
RCLCPP_INFO(this->get_logger(),"当前no_time章节存量为%zu:存量不够5章,开始等待",novels_queue.size());
//设置rate周期为3s,代表3s检查一次
rclcpp::Rate loop_rate(1.0/4);
//当书库的章节小于5时,一直循环
while(novels_queue.size() < 5)
{
//显示当前书库的章节数量及缺少数量
RCLCPP_INFO(this->get_logger(),"等待中,目前已有%zu章,还差%zu章",novels_queue.size(),5-novels_queue.size());
//让整个循环3s运行一次
loop_rate.sleep();
}
}
//当章节数量达到5,跳出循环
RCLCPP_INFO(this->get_logger(),"当前no_time小说存量为%zu,已满足需求,准备发送",novels_queue.size());
//第三步:把书发给张三
//把小说从书库中一章一章取出,并放进请求响应对象response中
if(sendnovel_flag == 1)
{
for(unsigned int i = 0 ; i < 5 ;i++)
{
response->novels.push_back(novels_queue.front());
novels_queue.pop();
}
}
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<ReadersAndSellServer>("wang5");
//rclcpp::spin(node);
//将之前的节点执行器换成多线程执行器
rclcpp::executors::MultiThreadedExecutor exector;
exector.add_node(node);
exector.spin();
rclcpp::shutdown();
return 0;
}
编写服务端的步骤
(1)导入服务接口
(2)创建服务端回调函数
(3)声明并创建服务端
(4)编写回调函数逻辑处理请求
(1)导入服务接口
第一步:修改CMakeLists.txt
find_package(learning_interfaces1 REQUIRED)
add_executable(wang5_node src/wang5.cpp)
ament_target_dependencies(wang5_node rclcpp std_msgs learning_interfaces1)
第二步:修改package.xml
<depend>learning_interfaces1</depend>
第三步:添加服务接口
//1.导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
(2)创建服务端回调函数
创建王五服务端回调函数,需要考虑两个方面:
第一,我们会收到张三的请求并进行处理。
第二,我们收到请求后要返回给张三一个响应。
根据我们上一篇的自定义接口SellNovel.srv
#Request部分:张三给王二的钱
uint32 money
---
#Response部分:王二给张三五章小说
string[] novels
所以我们回调函数应该有两个参数,一个是张三传入的请求参数(钱),另一个是我们返回给张三的响应参数(小说章节)
// 2、创建服务端回调函数
void SellNovel_callback(const learning_interfaces1::srv::SellNovel::Request::SharedPtr request,
const learning_interfaces1::srv::SellNovel::Response::SharedPtr response)
{
}
learning_interfaces1
:这部分表示的是包名(package name)。
srv
:这部分表示服务(service)的类型,用于标识这个数据类型是一个服务消息类型。在ROS中,服务消息类型有两部分,分别用于定义客户端请求和服务端响应的数据结构。
SellNovel
:是服务的名称
Request/Response
:这部分表示服务消息类型中的请求/响应部分。
money/novels
:具体的参数名,传入money/novels参数时,ROS2会将其识别为uint32/string[ ]类型的数据。
(3)声明并创建服务端
第一步:声明服务端
模版:rclcpp::Service<服务类型>::SharedPtr 服务端对象
rclcpp::Service<learning_interfaces1::srv::SellNovel>::SharedPtr sellserver;
第二步:创建服务端
模版1:服务端对象 = this->create_service<服务类型> ("服务名称",std::bind(&绑定回调函数,this,占位符1,占位符2...));
这种方式适用于一个简单的服务端,并且不需要额外的配置选项。
模版2:服务端对象 = this->create_service<服务类型> ("服务名称", std::bind(&绑定回调函数,this,占位符1,占位符2...), QoS配置,默认rmw_qos_profile_services_default, 指定服务请求处理的回调组);
这种方式适用于一个节点有多个回调函数,例如服务回调、订阅者回调和计时器回调,要使用多线程执行的情况。
根据题目要求,我们代码中目前有两个回调函数,分别是接收李四更新小说的回调函数和卖书的服务端回调函数。接收小说的回调函数有一个等待更新的程序,卖书的服务端回调函数中将要加入一个等待小说凑够五章的程序。
在ROS2中,单线程的执行模型中回调函数会顺序执行,一个执行完成才能执行下一个。当(卖书的服务端回调函数)由于等待条件(等待五章小说凑齐)而被阻塞时,其他回调函数(如接收小说的订阅回调函数)也将无法执行,程序就只能卡死在(等待小说凑齐)。一个服务端回调等不到小说傻傻等着,一个小说订阅者回调没法接收更新的小说,形成“死锁”的状态。(不理解的可以看看这篇文章:)为了解决这个问题,我们使用多线程执行器来让不同的回调函数在不同的线程中执行。
ROS2中要使用多线程执行器和回调组来实现多线程,首先我们先创建一个回调组,将服务端函数放进去。
创建回调组步骤:
(1)声明回调函数组
(2)创建回调函数组
声明回调函数组
rclcpp::CallbackGroup::SharedPtr callback_group_service;
创建回调函数组
callback_group_service = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
使用多线程执行器
最后,还需更改一下main
函数
//将之前的节点执行器换成多线程执行器
rclcpp::executors::MultiThreadedExecutor exector;
exector.add_node(node);
exector.spin();
创建服务端
// 4、创建服务端
sellserver = this->create_service<learning_interfaces1::srv::SellNovel>
("sell_novels",
std::bind(&&ReadersAndSellServer::SellNovel_callback,this,std::placeholders::_1,std::placeholders::_2),
rmw_qos_profile_default,
callback_group_service);
这段代码创建了一个服务端,用于提供一个叫做"sell_novels"
的服务,这个服务的类型是learning_interfaces1::srv::SellNovel
。当有客户端请求这个服务时,会调用名为SellNovel_callback
的函数来处理请求,这个函数会接收两个参数,分别是请求(request)和响应(response),所以用了两个占位符。使用默认的通信质量配置rmw_qos_profile_default
。服务端回调函数所属的回调组callback_group_service
,当调用回调函数请求时,把该回调函数单独放在这个回调组(也就是另一个线程)。
(4)编写回调函数逻辑处理请求
王五的“卖书”服务端收到张三“买书”请求时,应该怎么处理?
第一步:判断张三给的钱数是否足够(每章1元)
第二步:判断小说章节是否足够五章
第三步:把书发给张三
下面开始一步一步编写回调函数
第一步: 判断张三给的钱数是否足够(每章1元)
//第一步:判断张三给的钱数是否足够(每章1元)
unsigned int sendnovel_flag = 0; //是否发送小说标志位
unsigned int buymoney = request -> money; //张三发来的买书钱
if(buymoney == 5)
{
sendnovel_flag = 1;
}
else
{
sendnovel_flag = 0;
}
第二步: 判断小说章节是否足够五章
首先,我们需要建立一个书库,用来存入李四更新的小说,我们的小说类型是string
// 引入C++标准库中的队列容器模板(先进先出(FIFO)的数据结构)
#include <queue>
// 创建一个小说章节队列(书库)
std::queue<std::string> novels_queue;
其中,队列模板的基本形式为std::queue<T> N
,其中T
是队列中元素的类型,N
为队列名字。
N.push(元素)
: 在N队列尾部插入一个元素。
N.pop()
: 移除N队列头部的元素。
N.front()
: 返回N队列头部的元素(但不移除)。
N.back()
: 返回N队列尾部的元素(但不移除)。
N.empty()
: 如果N队列为空则返回 true,否则返回 false。
N.size()
: 返回N队列中元素的个数(队列长度)。
在订阅回调函数中,将李四更新的小说存入书库,novels_queue.push(novels->data)
//(4)创建订阅回调函数
void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
{
//(5)编写订阅回调函数
RCLCPP_INFO(this->get_logger(),"王五已阅读%s",novels->data.c_str());
//把收到的小说存入书库
novels_queue.push(novels->data);
}
接着继续编写回调函数
//第二步:判断小说章节是否足够五章
if(novels_queue.size() < 5)
{
RCLCPP_INFO(this->get_logger(),"当前no_time章节存量为%zu:存量不够5章,开始等待",novels_queue.size());
//设置rate周期为3s,代表3s检查一次
rclcpp::Rate loop_rate(1.0/3);
//当书库的章节小于5时,一直循环
while(novels_queue.size() < 5)
{
//显示当前书库的章节数量及缺少数量
RCLCPP_INFO(this->get_logger(),"等待中,目前已有%zu章,还差%zu章",novels_queue.size(),5-novels_queue.size());
//让整个循环3s运行一次
loop_rate.sleep();
}
}
//当章节数量达到5,跳出循环
RCLCPP_INFO(this->get_logger(),"当前no_time小说存量为%zu,已满足需求,准备发送",novels_queue.size());
第三步: 把书发给张三
//第三步:把书发给张三
//把小说从书库中一章一章取出,并放进请求响应对象response中
if(sendnovel_flag == 1)
{
for(unsigned int i = 0 ; i < 5 ;i++)
{
response->novels.push_back(novels_queue.front());
novels_queue.pop();
}
}
到此,就完成了王五服务端的编写。
3.编写张三客户端代码(买书)
完整代码
#include "rclcpp/rclcpp.hpp"
// 1、导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
class BuyNovelClient : public rclcpp::Node
{
public:
BuyNovelClient(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"买书客户端%s已启动",name.c_str());
// 5、创建客户端
buyclient = this->create_client<learning_interfaces1::srv::SellNovel>("sell_novels");
}
// 2、创建请求函数
void buy_novel()
{
// 6、编写请求函数
RCLCPP_INFO(this->get_logger(),"去找王五买小说啦!!");
//等待服务端上线
while(!buyclient->wait_for_service(std::chrono::seconds(3))) //若上线则退出循环
{
RCLCPP_INFO(this->get_logger(),"等待卖书服务端上线中...");
}
//构造请求数据
auto request = std::make_shared<learning_interfaces1::srv::SellNovel_Request>(); //创建了一个“钱袋”
request->money = 5; //在“钱袋”里装入了5块钱
//发送异步请求
buyclient ->async_send_request(request,std::bind(&BuyNovelClient::buynovel_callback,this,std::placeholders::_1));
}
private:
// 4、声明客户端
rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedPtr buyclient;
// 3、创建请求结果接收回调函数
void buynovel_callback(rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedFuture response)
{
// 7、编写请求结果接收回调函数
//获取回复的结果
auto buy_result = response.get();
RCLCPP_INFO(this->get_logger(),"已收到王五发来的小说共%zu章,准备开读",buy_result->novels.size());
//打印出小说内容
for(std::string novels : buy_result->novels)
{
RCLCPP_INFO(this->get_logger(),"在读:%s",novels.c_str());
}
RCLCPP_INFO(this->get_logger(), "小说读完了,好期待下面的章节呀!");
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<BuyNovelClient>("zhang3");
//修改main函数调用请求函数
node->buy_novel();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
编写客户端代码的步骤:
(1)导入服务接口
(2)创建请求函数
(3)创建结果接收回调函数
(4)声明客户端
(5)创建客户端
(6)编写请求函数
(7)编写结果接收回调函数
(8)修改main函数调用请求函数
(1)导入服务接口
同编写服务端。
修改CMakeLists.txt
add_executable(zhang3_node src/zhang3.cpp)
ament_target_dependencies(zhang3_node rclcpp learning_interfaces1)
导入头文件
// 1、导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
(2)创建请求函数
// 2、创建请求函数
void buy_novel()
{
}
(3)创建请求结果接收回调函数
// 3、创建请求结果接收回调函数
void buynovel_callback(rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedFuture response)
{
}
当使用服务进行异步通信时,客户端(client)发送一个请求(request)到服务器(server),并且不会立即得到响应。这是因为服务器可能需要一些时间来处理这个请求。在服务器处理请求并准备好响应之前,客户端需要有一种方式来“等待”这个响应。
rclcpp::Client<服务类型>::SharedFuture
就是用来处理这种情况的。这里的Future
是一个表示“未来某个时间点结果”的对象。当你发送一个异步服务请求时,你会立即得到一个Future
对象,而不是服务响应本身。这个Future
对象允许你在响应可用时获取它,同时允许你的代码继续执行其他任务,而不是阻塞等待响应。
当服务响应到达时,ROS2会自动调用这个函数,并将包含响应结果作为参数传递给它response
。
(4)声明客户端
声明客户端模版代码
rclcpp::Client<服务类型>::SharedPtr 客户端对象;
// 4、声明客户端
rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedPtr buyclient;
(5)创建客户端
创建客户端的模板代码
客户端对象 = this->create_client<服务类型>("服务名称");
// 5、创建客户端
buyclient = this->create_client<learning_interfaces1::srv::SellNovel>("sell_novels");
(6) 编写请求函数
编写请求函数,主要分为三步
第一步:等待服务端上线
第二步:构造请求数据
第三步:发送异步请求
第一步: 等待服务端上线
RCLCPP_INFO(this->get_logger(),"去找王五买小说啦!!");
//等待服务端上线
while(!buyclient->wait_for_service(std::chrono::seconds(3))) //若上线则退出循环
{
RCLCPP_INFO(this->get_logger(),"等待卖书服务端上线中...");
}
wait_for_service
:这是rclcpp::ClientBase
类的一个成员函数,用于等待服务变得可用。
std::chrono::seconds(1)
:这是一个时间间隔,表示一秒。这是wait_for_service
函数的参数,指定了函数应该等待服务变得可用的时间长度。
client_->wait_for_service(std::chrono::seconds(1))
:会返回一个布尔值,表示服务是否在给定的时间(这里是1秒)内变得可用。如果服务变得可用,则返回true
;如果服务在指定的时间内仍然不可用,则返回false
。
当你在这个表达式前面加上!
时,你就将这个布尔值反转了。因此,while (!client_->wait_for_service(std::chrono::seconds(1)))
的意思是“当服务在1秒内没有变得可用时,继续循环”。这意味着循环将一直执行,直到服务变得可用为止。一旦服务变得可用,wait_for_service
将返回 true
,!wait_for_service
将返回 false
,从而退出循环。
第二步: 构造请求数据
//构造请求数据
auto request = std::make_shared<learning_interfaces1::srv::SellNovel_Request>(); //创建了一个“钱袋”
request->money = 5; //在“钱袋”里装入了5块钱
在发送服务请求之前,通常需要创建一个请求对象,并填充请求所需的数据。
在这,使用std::make_shared
创建了一个请求对象request
,并设置了购买小说的金额为5。
std::make_shared<learning_interfaces1::srv::SellNovel_Request>()
类似于一个空的容器,用于存放你的请求信息,也可以被看作是客户端提交给服务端的请求表单。SellNovel_Request
表单中定义了一个填写金额的money
,那么request->money = 5
就相当于装入5块钱,告诉服务端我们希望购买的小说的金额是5块钱。
第三步: 发送异步请求
//发送异步请求
buyclient ->async_send_request(request,std::bind(&BuyNovelClient::buynovel_callback,this,std::placeholders::_1));
发送服务请求时,通常使用async_send_request
函数发送异步请求,并设置一个回调函数来处理服务端的响应。
在这,使用async_send_request
发送请求,它包含两个参数:一个服务请求对象和一个回调函数。
request
:这是一个指向服务请求对象的共享指针,在上一步相当于创建一个“钱袋”。它包含了要发送给服务的数据。
buynovel_callback
:作为回调函数绑定到异步请求上。一旦服务端返回了结果,回调函数会被自动调用以处理响应数据。
(7)编写请求结果接收回调函数
第一步: 获取回复的结果
//获取回复的结果
auto buy_result = response.get();
RCLCPP_INFO(this->get_logger(),"已收到王五发来的小说共%zu章,准备开读",buy_result->novels.size());
该回调函数,传入的参数是rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedFuture response
,response
是一个SharedFuture
对象,这个对象表示一个“未来的”结果,即服务响应的结果(在服务响应到达时变得可用)。可通过调用.get()
方法可以获取异步请求的结果。
auto
关键字用于自动推断buy_result
的类型,将获取到的结果赋值给buy_result
。buy_result->novels.size()
即可获取返回结果中小说的章节数。
response.get()
;这部分代码的作用就是“等待”并“获取”那个“未来的”结果。
第二步: 打印出小说内容
//打印出小说内容
for(std::string novels : buy_result->novels)
{
RCLCPP_INFO(this->get_logger(),"在读:%s",novels.c_str());
}
RCLCPP_INFO(this->get_logger(), "小说读完了,好期待下面的章节呀!");
这是一个循环,用于遍历buy_result
中的novels
列表,并打印出每一章的内容。因为novels
是字符串组string[ ]
类型,所以会一次性打印出发送来的五个章节。
(8)修改main函数调用请求函数
//修改main函数调用请求函数
node->buy_novel();
至此,就完成了客户端代码的编写。
四、完整代码
SellNovel.srv
#张三给王五的钱
uint32 money
---
#王五给张三五章小说
string[] novels
wang5.cpp
#include "rclcpp/rclcpp.hpp"
//(1)导入订阅的话题接口类型
#include "std_msgs/msg/string.hpp"
//1.导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
// 引入C++标准库中的队列容器模板(先进先出(FIFO)的数据结构)
#include <queue>
class ReadersAndSellServer : public rclcpp::Node
{
public:
ReadersAndSellServer(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"读者节点%s已启动",name.c_str());
RCLCPP_INFO(this->get_logger(),"卖书服务端%s已启动",name.c_str());
//(3)创建订阅者
sub_readers = this->create_subscription<std_msgs::msg::String>
("no_time",10,std::bind(&ReadersAndSellServer::readnovels_callback,this,std::placeholders::_1));
// 5、创建一个服务回调组
callback_group_service = this->create_callback_group(rclcpp::CallbackGroupType::MutuallyExclusive);
// 4、创建服务端
sellserver = this->create_service<learning_interfaces1::srv::SellNovel>
("sell_novels",
std::bind(&ReadersAndSellServer::SellNovel_callback,this,std::placeholders::_1,std::placeholders::_2),
rmw_qos_profile_default,
callback_group_service);
}
private:
//(2)声明订阅者
rclcpp::Subscription<std_msgs::msg::String>::SharedPtr sub_readers;
// 3、声明服务端
rclcpp::Service<learning_interfaces1::srv::SellNovel>::SharedPtr sellserver;
// 4、声明一个服务回调组
rclcpp::CallbackGroup::SharedPtr callback_group_service;
// 创建一个小说章节队列(书库)
std::queue<std::string> novels_queue;
//(4)创建订阅回调函数
void readnovels_callback(const std_msgs::msg::String::SharedPtr novels)
{
//(5)编写订阅回调函数
RCLCPP_INFO(this->get_logger(),"王五已阅读%s",novels->data.c_str());
//把收到的小说存入书库
novels_queue.push(novels->data);
}
// 2、创建服务端回调函数
void SellNovel_callback(const learning_interfaces1::srv::SellNovel::Request::SharedPtr request,
const learning_interfaces1::srv::SellNovel::Response::SharedPtr response)
{
//第一步:判断张三给的钱数是否足够(每章1元)
unsigned int sendnovel_flag = 0; //是否发送小说标志位
unsigned int buymoney = request -> money; //张三发来的买书钱
if(buymoney == 5)
{
sendnovel_flag = 1;
RCLCPP_INFO(this->get_logger(),"已收到张三%d元",buymoney);
}
else
{
sendnovel_flag = 0;
}
//第二步:判断小说章节是否足够五章
if(novels_queue.size() < 5)
{
RCLCPP_INFO(this->get_logger(),"当前no_time章节存量为%zu:存量不够5章,开始等待",novels_queue.size());
//设置rate周期为3s,代表3s检查一次
rclcpp::Rate loop_rate(1.0/3);
//当书库的章节小于5时,一直循环
while(novels_queue.size() < 5)
{
//显示当前书库的章节数量及缺少数量
RCLCPP_INFO(this->get_logger(),"等待中,目前已有%zu章,还差%zu章",novels_queue.size(),5-novels_queue.size());
//让整个循环3s运行一次
loop_rate.sleep();
}
}
//当章节数量达到5,跳出循环
RCLCPP_INFO(this->get_logger(),"当前no_time小说存量为%zu,已满足需求,准备发送",novels_queue.size());
//第三步:把书发给张三
//把小说从书库中一章一章取出,并放进请求响应对象response中
if(sendnovel_flag == 1)
{
for(unsigned int i = 0 ; i < 5 ;i++)
{
response->novels.push_back(novels_queue.front());
novels_queue.pop();
}
}
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<ReadersAndSellServer>("wang5");
//rclcpp::spin(node);
//将之前的节点执行器换成多线程执行器
rclcpp::executors::MultiThreadedExecutor exector;
exector.add_node(node);
exector.spin();
rclcpp::shutdown();
return 0;
}
zhang3.cpp
#include "rclcpp/rclcpp.hpp"
// 1、导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
class BuyNovelClient : public rclcpp::Node
{
public:
BuyNovelClient(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"买书客户端%s已启动",name.c_str());
// 5、创建客户端
buyclient = this->create_client<learning_interfaces1::srv::SellNovel>("sell_novels");
}
// 2、创建请求函数
void buy_novel()
{
// 6、编写请求函数
RCLCPP_INFO(this->get_logger(),"去找王五买小说啦!!");
//等待服务端上线
while(!buyclient->wait_for_service(std::chrono::seconds(3))) //若上线则退出循环
{
RCLCPP_INFO(this->get_logger(),"等待卖书服务端上线中...");
}
//构造请求数据
auto request = std::make_shared<learning_interfaces1::srv::SellNovel_Request>(); //创建了一个“钱袋”
request->money = 5; //在“钱袋”里装入了5块钱
//发送异步请求
buyclient ->async_send_request
(request,std::bind(&BuyNovelClient::buynovel_callback,this,std::placeholders::_1));
}
private:
// 4、声明客户端
rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedPtr buyclient;
// 3、创建请求结果接收回调函数
void buynovel_callback(rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedFuture response)
{
// 7、编写请求结果接收回调函数
//获取回复的结果
auto buy_result = response.get();
RCLCPP_INFO(this->get_logger(),"已收到王五发来的小说共%zu章,准备开读",buy_result->novels.size());
//打印出小说内容
for(std::string novels : buy_result->novels)
{
RCLCPP_INFO(this->get_logger(),"在读:%s",novels.c_str());
}
RCLCPP_INFO(this->get_logger(), "小说读完了,好期待下面的章节呀!");
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<BuyNovelClient>("zhang3");
//修改main函数调用请求函数
node->buy_novel();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
另一个版本的zhang3.cpp
,,加入了一个定时器,间隔十秒发送一次请求
#include "rclcpp/rclcpp.hpp"
// 1、导入服务接口
#include "learning_interfaces1/srv/sell_novel.hpp"
class BuyNovelClient : public rclcpp::Node
{
public:
BuyNovelClient(std::string name) : Node(name)
{
RCLCPP_INFO(this->get_logger(),"买书客户端%s已启动",name.c_str());
// 5、创建客户端
buyclient = this->create_client<learning_interfaces1::srv::SellNovel>("sell_novels");
// 10、创建定时器
sendbuy_timer = this->create_wall_timer
(std::chrono::milliseconds(10000), std::bind(&BuyNovelClient::buy_novel, this));
}
// 2、创建请求函数
void buy_novel()
{
// 6、编写请求函数
RCLCPP_INFO(this->get_logger(),"去找王五买小说啦!!");
//等待服务端上线
while(!buyclient->wait_for_service(std::chrono::seconds(3))) //若上线则退出循环
{
RCLCPP_INFO(this->get_logger(),"等待卖书服务端上线中...");
}
//构造请求数据
auto request = std::make_shared<learning_interfaces1::srv::SellNovel_Request>(); //创建了一个“钱袋”
request->money = 5; //在“钱袋”里装入了5块钱
//发送异步请求
buyclient ->async_send_request
(request,std::bind(&BuyNovelClient::buynovel_callback,this,std::placeholders::_1));
}
private:
// 4、声明客户端
rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedPtr buyclient;
// 9、声明定时器
rclcpp::TimerBase::SharedPtr sendbuy_timer;
// 3、创建请求结果接收回调函数
void buynovel_callback(rclcpp::Client<learning_interfaces1::srv::SellNovel>::SharedFuture response)
{
// 7、编写请求结果接收回调函数
//获取回复的结果
auto buy_result = response.get();
RCLCPP_INFO(this->get_logger(),"已收到王五发来的小说共%zu章,准备开读",buy_result->novels.size());
//打印出小说内容
for(std::string novels : buy_result->novels)
{
RCLCPP_INFO(this->get_logger(),"在读:%s",novels.c_str());
}
RCLCPP_INFO(this->get_logger(), "小说读完了,好期待下面的章节呀!");
}
};
int main(int argc,char** argv)
{
rclcpp::init(argc,argv);
auto node = std::make_shared<BuyNovelClient>("zhang3");
//修改main函数调用请求函数
node->buy_novel();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
五、编译
打开三个终端
第一个
colcon build
source install/setup.sh
ros2 run learning_service zhang3_node
第二个
source install/setup.sh
ros2 run learning_service wang5_node
第三个
source install/setup.sh
ros2 run learning_topic li4_node
运行结果
总结
自用