ROS客户端Client与服务端Server的编程实现
前情
之前学习了ROS节点中订阅者和发布者通讯方式,接下来了解的是另外一种通讯方式,也就是客户端和服务器之间的通讯。
包含客户端和服务端的服务模型(客户端/服务器)如下图(来自古月居ROS21讲)

建立客户端
- 进入src文件夹,新建功能包
~/catkin_ws/src$ catkin_create_pkg learning_service roscpp rospy std_msgs geometry_msgs turtlesim
其中后面的几项是功能包需要依赖的库。
- 在功能包/src下创建文件
touch turtle_spawn.cpp
- 键入代码
代码来自古月居ROS21讲
#include <ros/ros.h>
#include <turtlesim/Spawn.h>1
int mian(int argc, char *argv)
{
//init ros node
ros::init(argc,argv,"turtle_spawn");
//create nodeHandle
ros::NodeHandle node;
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 spawn 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("Spawn turtle successfully [name:%s]",srv.response.name.c_str());
return 0;
}
代码解释
1.
ros::service::waitForService("/spawn");
这是一个阻塞型程序,一直等待 /spawn 的服务器存在,只有感知到这个服务器,程序才会继续进行
2.
ros::ServiceClient add_turtle= node.serviceClient<turtlesim::Spawn>("/spawn");
实例化一个ServiceClient类的对象,创建了一个客户端,这个客户端是给 /spawn 这个服务发送请求的,请求的数据类型为 turtlesim::Spawn
3.
创建了客户端之后,紧接着就是封装请求数据,下面的程序就是定义了请求数据中的数据内容:
turtlesim::Spawn srv;
srv.request.x=2.0;
srv.request.y=2.0;
srv.request.name="turtle2";
turtlesim::Spawn 除了xy之外还有海龟的角度θ,如果不定义,默认为0;
请求的目的就是新生成一个海龟,这个海龟的信息就是xy,name
4.
ROS_INFO("Call service to spawn turtle[x:%0.6f,y:%0.6f,name:%s]",srv.request.x,srv.request.y,srv.request.name.c_str());
发给终端的日志信息,告诉我们已经发出请求了
5.
add_turtle.call(srv);
对服务端发出请求的指令
call()也是一个阻塞型函数,将request发出去之后一直等待直到
ROS_INFO("Spawn turtle successfully [name:%s]",srv.response.name.c_str());
显示服务器的回应
- 更改编译规则
打开CMakeList.txt,修改编译规则

- 回到根目录,编译
编译成功后,在/devel/lib/下就会出现对应的功能包名字的文件夹,里面存放着对应新的包的可执行文件
-测试,
$ roscore
$ rosrun turtlesim turtlesim_node
$ rosrun learning_service turtle_spawn
建立服务端
在src文件夹中创建新文件,编辑代码
注意,要包含以下头文件
#include<ros/ros.h>
#include<geometry_msgs/Twist.h>
#include<std_srvs/Trigger.h>
| 头文件 | 说明 |
|---|---|
| #include<geometry_msgs/Twist.h> | 包含控制小海龟速度指令的头文件 |
| #include<std_srvs/Trigger.h> | 包含服务数据类型std_srvs/Trigger 的头文件 |
全部代码为
代码来自古月居ROS21讲
#include<ros/ros.h>
#include<geometry_msgs/Twist.h>
#include<std_srvs/Trigger.h>
//声明全局对象和变量,这是因为在主函数和回调函数中都需要用到
ros::Publisher turtle_vel_pub;
bool pubCommand = false;//pubCommand作为一个flag标志位,标志海龟是运动还是停止
//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::init(argc,argv,"turtle_command_server");
ros::NodeHandle n;
// 创建一个服务器,这跟订阅者类似,因为不知道客户端的请求什么时候进来,
//所以通过一个回调函数的机制来处理request指令,
//一旦服务端收到针对服务名字"/turtle_command"的request请求之后
//立刻会调用回调函数,在回调函数里针对request的内容的处理
//比如这里是想通过客户端发送请求让小海龟动起来,这里就是在回调函数里让海龟运动
ros::ServiceServer command_service = n.advertiseService("/turtle_command",commandCallback);
//除了创建了服务端之外还创建了一个发布者,用于控制小海龟
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();
//如果pubCommand是false的话,就不会对小海龟发出指令
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;
}
代码解释在代码块的注释中
ros::spin() 和 ros::spinOnce() 称为ROS消息回调处理函数,通常会出现在ROS的主循环中,程序需要不断调用ros::spin() 或 ros::spinOnce(),两者区别在于前者调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而后者在调用后还可以继续执行之后的程序。
如果你的程序写了相关的消息订阅函数,那么程序在执行过程中,除了主程序以外,ROS还会自动在后台按照你规定的格式,接受订阅的消息,但是所接到的消息并不是立刻就被处理,而是必须要等到ros::spin()或ros::spinOnce()执行的时候才被调用,这就是消息回到函数的原理。
就像上面说的,ros::spin() 在调用后不会再返回,也就是你的主程序到这儿就不往下执行了,而 ros::spinOnce() 后者在调用后还可以继续执行之后的程序。
其实看函数名也能理解个差不多,一个是一直调用;另一个是只调用一次,如果还想再调用,就需要加上循环了。
这里一定要记住,ros::spin()函数一般不会出现在循环中,因为程序执行到spin()后就不调用其他语句了,也就是说该循环没有任何意义,还有就是spin()函数后面一定不能有其他语句(return 0 除外),有也是白搭,不会执行的。ros::spinOnce()的用法相对来说很灵活,但往往需要考虑调用消息的时机,调用频率,以及消息池的大小,这些都要根据现实情况协调好,不然会造成数据丢包或者延迟的错误。
spin()是一直调用被调用,直到ctrl+c; 而 ros::spinOnce() 是,如果还想再调用,就需要加上循环语句 while(ros::ok())。
对于ros::spinOnce(),有些传输特别快的消息,尤其需要注意合理控制消息池大小和ros::spinOnce()执行频率; 比如:消息发送频率为10Hz,那么 当ros::spinOnce()的调用频率为5Hz,那么接收端消息池的大小就一定要大于2,才能保证数据不丢失,无延迟。
- 修改编译规则
打开CMakeList.txt, 加入如下语句

- 编译
回到根目录,编译 - 设置环境变量
- 检查环境变量
- 测试
$ roscore
$ rosrun turtlesim turtlesim_node
$ rosrun learning_service turtle_command_server
$ rosservice call /turtle_command "{}"
其中 /turtle_command 是刚才程序中定义的server服务端名称 , 即发送请求,当spinOnce()扫描到请求之后,执行回调。小海龟就动啦!
rosservice ros服务
COMMAND EXPAIN rosservice list 显示活动的服务信息 rosservice info [服务名称] 显示指定服务的信息 rosservice type [服务名称] 显示服务类型 rosservice find [服务类型] 查找指定服务类型的服务 rosservice uri [服务名称] 显示ROSRPC URI服务 rosservice args [服务名称] 显示服务参数 rosservice call [服务名称] [参数] 用输入的参数请求服务
本文深入讲解ROS中的服务通信机制,包括客户端与服务端的编程实现。通过实例演示如何使用ROS客户端向服务端发送请求,以及服务端如何响应并执行相应操作。同时,介绍了ROS服务相关的常用命令。

8892

被折叠的 条评论
为什么被折叠?



