上一篇博客我们介绍了ROS的通信方式中的topic(话题)通信,我们知道topic是ROS中的一种单向的异步通信方式。然而有些时候单向的通信满足不了通信要求,比如当一些节点只是临时而非周期性的需要某些数据,如果用topic通信方式时就会消耗大量不必要的系统资源,造成系统的低效率高功耗。
这种情况下,就需要有另外一种请求-查询式的通信模型。这篇博客我们来介绍ROS通信中的另一种通信方式——service(服务)。
service方式在通信模型上与topic做了区别。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等待过程中,是处于阻塞状态的成通信。这样的通信模型没有频繁的消息传递,没有冲突与高系统资源的占用,只有接受请求才执行服务,简单而且高效。
我们对比一下这两种最常用的通信方式,加深我们对两者的理解和认识,具体见下表:
注意:远程过程调用(Remote Procedure Call,RPC),可以简单通俗的理解为在一个进程里调用另一个进程的函数。在下面的具体示例中也有体现。
首先我们来实现下面的服务模型,主要的功能就是实现在我们的小海龟仿真客户端下添加一只新的小海龟。serve端是小海龟仿真器,client端则是本节中我们需要实现的功能。client主要用来发布一个生成小海龟的请求给serve端,serve端在完成请求后会反馈一个response给client端。它们之间的通讯桥梁就是我们的Service,在这个例子中就是spawn,整个的管理者依旧是Rosmaster。
cd catkin_ws/src/
catkin_create_pkg learning_service roscpp rospy std_msgs geometry_msgs turtlesim
进入src文件夹,新建一个turtle_spawn.cpp
,将以下代码拷贝进去:
/**
* 该例程将请求/spawn服务,服务数据类型turtlesim::Spawn
*/
#include
#include
int main(int argc, char** argv)
{
// 初始化ROS节点
ros::init(argc, argv, "turtle_spawn");
// 创建节点句柄
ros::NodeHandle node;
// 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
ros::service::waitForService("/spawn");
ros::ServiceClient add_turtle = node.serviceClient<turtlesim::Spawn>("/spawn");
// 初始化turtlesim::Spawn的请求数据
turtlesim::Spawn srv;
srv.request.x = 2.0;
srv.request.y = 2.0;
srv.request.name = "turtle2";
// 请求服务调用
ROS_INFO("Call service to spwan turtle[x:%0.6f, y:%0.6f, name:%s]",
srv.request.x, srv.request.y, srv.request.name.c_str());
add_turtle.call(srv);
// 显示服务调用结果
ROS_INFO("Spwan turtle successfully [name:%s]", srv.response.name.c_str());
return 0;
};
需要指出的是,ros::service::waitForService("/spawn");
这行语句是一个阻塞型的API,如果没有这个spawn的例程就会一直等待下去,就跟上网一样,如果请求的服务器不存在的话,我们是不可能连接成功的。add_turtle.call(srv);
也是一个阻塞型函数,它会把请求发出去,然后就一直在那等待服务器给的反馈;跟上网浏览网页也是一个道理,我们访问网站,经常会有一个小圆圈在那里一直转,一直等到有响应以后才会刷新出来。
随后是我们的一些编译的规则,跟之前一样,在CMakeLists.txt
的相应位置添加以下语句:
add_executable(turtle_spawn src/turtle_spawn.cpp)
target_link_libraries(turtle_spawn ${catkin_LIBRARIES})
随后回到catkin_ws进行编译即可,在相关的目录下可以看到编译完成的可执行文件:
由于每次都要刷新环境变量很麻烦,所以我们可以直接把环境变量写到.bashrc中:首先打开终端,进入catkin_ws,然后用pwd显示当前目录:
然后用显示的目录+devel/setup.bash,就是我们要像.bashrc中添加的路径。最后在home目录下同时按住ctrl+h,显示隐藏文件,找到.bashrc文件并且打开,拖到最下面,添加:
source /home/wh/catkin_ws/devel/setup.bash
这样操作之后,就不需要每次都source了。
现在我们可以直接打开新终端,启动该roscore,然后执行:rosrun turtlesim turtlesim_node
和rosrun learning_service turtle_spawn
就可以看的我们想要的结果:
这一节我们来实现服务端server的功能。首先是server通过Topic来给小海龟发送速度的指令,client端相当于一个开关,发布request给server,从而控制server,让server要不要给小海龟发指令。其中所用的Service的名字是我们自己定义的/turtle_command,数据类型是ROS中自带的Trigger
(触发)。本节既涉及到serve的实现,也涉及到topic的实现,所以本节的代码还是相对比较复杂的。
首先依旧是在src文件中创建turtle_command_server.cpp
文件,将以下代码copy进去:
/**
* 该例程将执行/turtle_command服务,服务数据类型std_srvs/Trigger
*/
#include
#include
#include
ros::Publisher turtle_vel_pub;
bool pubCommand = false;
// service回调函数,输入参数req,输出参数res
bool commandCallback(std_srvs::Trigger::Request &req,
std_srvs::Trigger::Response &res)
{
pubCommand = !pubCommand;
// 显示请求数据
ROS_INFO("Publish turtle velocity command [%s]", pubCommand==true?"Yes":"No");
// 设置反馈数据
res.success = true;
res.message = "Change turtle command state!";
return true;
}
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "turtle_command_server");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个名为/turtle_command的server,注册回调函数commandCallback
ros::ServiceServer command_service = n.advertiseService("/turtle_command", commandCallback);
// 创建一个Publisher,发布名为/turtle1/cmd_vel的topic,消息类型为geometry_msgs::Twist,队列长度10
turtle_vel_pub = n.advertise<geometry_msgs::Twist>("/turtle1/cmd_vel", 10);
// 循环等待回调函数
ROS_INFO("Ready to receive turtle command.");
// 设置循环的频率
ros::Rate loop_rate(10);
while(ros::ok())
{
// 查看一次回调函数队列
ros::spinOnce();
// 如果标志为true,则发布速度指令
if(pubCommand)
{
geometry_msgs::Twist vel_msg;
vel_msg.linear.x = 0.5;
vel_msg.angular.z = 0.2;
turtle_vel_pub.publish(vel_msg);
}
//按照循环频率延时
loop_rate.sleep();
}
return 0;
}
主要注意serve的创建以及里面commandCallback
的机制。
随后就可以进行编译了,当然了,需要事先配置好CMakeLists.txt里的相关规则:
add_executable(turtle_command_server src/turtle_command_server.cpp)
target_link_libraries(turtle_command_server ${catkin_LIBRARIES})
cd ~/catkin_ws
catkin_make
source devel/setup.bash//这一步可以省略了,因为我们已经在.bashrc中设置过了
roscore
rosrun turtlesim turtlesim_node
rosrun learning_service turtle_command_server
rosservice call /turtle_command "{}"
随后只要反复输入rosservice call /turtle_command "{}"
便可以控制小海龟的启停:
上面两节我们使用了ROS定义好的spawn和trigger两种服务类型数据,本节介绍如何根据自己的需求定制服务数据并且使用。
通过RosMaster管理本节需要实现的Client和Server,Client端要做的是发布一个显示一个人信息的Request,并且把个人信息用Service的数据格式发出去;然后在Serve端我们就会收到这样的Request,同时会包含一个人的基本信息,包括性别年龄等,通过日志显示出来后,会通过Response反馈显示结果;
其中用到的Service叫做show_person,是我们自己定义的,数据类型是我们自定义的learning_service::Person
。
参考上一篇博客我们自定义的.msg文件,这里主要定义的就是.srv文件,主要的区别在于service需要一个response,所以我们需要用—做分割,—以上的是request数据,—以下的是response数据,这样ros在编译时就会产生对应的头文件。
首先在learning_service文件夹中创建srv文件夹,随后在srv文件夹中touch Person.srv
,如下图所示:
然后打开srv文件,加以下代码添加进去:
string name
uint8 age
uint8 sex
uint8 unknown = 0
uint8 male = 1
uint8 female = 2
---
string result
随后进行编译,同样的,需要在CMakeLists.txt和package.xml中添加相关的编译设置。
在package.xml中添加功能包依赖
<build_depend>message_generation</build_depend>
<exec_depend>message_runtime</exec_depend>
在CMakeLists.txt添加编译选项
find_package( ……message_generation)
add_service_files(FILES Person.srv)
generate_messages(DEPENDENCIES std_msgs)
catkin_package(…… message_runtime)
然后回到catkin_ws下进行catkin_make进行编译:
可以在相关目录下找到编译生成的文件:
随后我们就可以在自己的代码中来调用这些头文件了。
在下图所示目录下创建两个cpp文件,代码在下面:
/**
* 该例程将请求/show_person服务,服务数据类型learning_service::Person
*/
#include
#include "learning_service/Person.h"
int main(int argc, char** argv)
{
// 初始化ROS节点
ros::init(argc, argv, "person_client");
// 创建节点句柄
ros::NodeHandle node;
// 发现/spawn服务后,创建一个服务客户端,连接名为/spawn的service
ros::service::waitForService("/show_person");
ros::ServiceClient person_client = node.serviceClient<learning_service::Person>("/show_person");
// 初始化learning_service::Person的请求数据
learning_service::Person srv;
srv.request.name = "Tom";
srv.request.age = 20;
srv.request.sex = learning_service::Person::Request::male;
// 请求服务调用
ROS_INFO("Call service to show person[name:%s, age:%d, sex:%d]",
srv.request.name.c_str(), srv.request.age, srv.request.sex);
person_client.call(srv);
// 显示服务调用结果
ROS_INFO("Show person result : %s", srv.response.result.c_str());
return 0;
};
/**
* 该例程将执行/show_person服务,服务数据类型learning_service::Person
*/
#include
#include "learning_service/Person.h"
// service回调函数,输入参数req,输出参数res
bool personCallback(learning_service::Person::Request &req,
learning_service::Person::Response &res)
{
// 显示请求数据
ROS_INFO("Person: name:%s age:%d sex:%d", req.name.c_str(), req.age, req.sex);
// 设置反馈数据
res.result = "OK";
return true;
}
int main(int argc, char **argv)
{
// ROS节点初始化
ros::init(argc, argv, "person_server");
// 创建节点句柄
ros::NodeHandle n;
// 创建一个名为/show_person的server,注册回调函数personCallback
ros::ServiceServer person_service = n.advertiseService("/show_person", personCallback);
// 循环等待回调函数
ROS_INFO("Ready to show person informtion.");
ros::spin();
return 0;
}
随后再进行编译即可,同样的,需要在CMakeLists.txt中添加相关的编译规则:
add_executable(person_server src/person_server.cpp)
target_link_libraries(person_server ${catkin_LIBRARIES})
add_dependencies(person_server ${PROJECT_NAME}_gencpp)
add_executable(person_client src/person_client.cpp)
target_link_libraries(person_client ${catkin_LIBRARIES})
add_dependencies(person_client ${PROJECT_NAME}_gencpp)
cd ~/catkin_ws
catkin_make
source devel/setup.bash
roscore
rosrun learning_service person_server
rosrun learning_service person_client
编译成功后可以在相关目录下看到生成的文件:
运行成功如下:
当然这里也可以先运行client端,因为有一个waitForService
,所以可以达到一样的效果,只不过涉及到一个谁先谁等谁的问题:
说个题外话,当运行了很多ros程序时,要记得及时把roscore关掉,因为里面有一个参数服务器,存储了很多个参数,所以有时候可能会莫名其妙的报错,这是新手经常遇见的一个问题,所以要记得新开一个例程时要关掉roscore然后重新启动。
至此,我们已经完成了Topic和Service两种通信机制的学习,下篇博客我们将学习参数服务器的使用与编程方法。