本篇博客是慕课《ROS机器人操作系统入门》的第三篇笔记,慕课网址是这里。本篇笔记主要记录Service在C++和Python下的两种实现方式以及需要注意的问题。
1.任务描述与分析
任务是在之前利用WebSocket实现过的模拟登录功能。用户在客户端输入用户名和密码并发送给服务器端,服务器端收到用户名和密码后将其与正确的进行比较,若一致返回登录成功,否则失败。
针对这种需求很明显使用Service-Client通信方式最为合适。具体而言,可以新建一个叫login的包(package),这个包中有server_login和client_login两个节点(Node),server_login用于处理用户发送的用户名和密码,并返回登录结果,
为了方便,定义用户名是xuhui,密码是123456,这样才能登录成功。
client_login用于客户端发送用户名和密码。定义Service的名称是login_info,Service消息的文件名是user.srv,格式是string类型的username、password以及返回string类型的登录状态state。
2.Service的C++实现
以下步骤是在创建好了工作空间(本机工作空间路径/root/catkin_ws)的情况下继续的,如果还没有创建好工作空间,可以参考这篇笔记创建工作空间,然后再回来。
(1)创建Package
创建Package有多种方式,这里介绍最原始的手动和使用RoboWare这个IDE的两种不同方式。
a.终端中手动创建
在终端中输入如下内容即可创建一个Package。
cd catkin_ws/src/
catkin_create_pkg login roscpp std_msgs message_generation message_runtime
第一行是切换到src目录下,第二行是在当前目录下创建一个叫login的包,并且包含roscpp、std_msgs、message_generation、message_runtime依赖,后两个是用到Service的包都要包含的依赖。
b.RoboWare自动创建
打开RoboWare,在”资源管理器”中找到src文件夹,右键单击选择”新建ROS包”,输入名称即可创建Package。创建完成后右键Package选择”编辑依赖的ROS包列表”,输入roscpp、std_msgs、message_generation、message_runtime(空格隔开)即可添加依赖。
(2)创建自定义消息文件
a.手动创建文件
在login包的根目录下新建一个srv文件夹,在其中新建一个叫做user.srv的文件即可。
b.RoboWare创建文件
在”资源管理器”中找到login文件夹,右键单击选择”新建Srv文件夹”,在新建的srv文件夹上右键选择”新建SRV文件”,输入名称即可创建。
以上两种方式文件创建好后在文件中写入如下内容并保存。
string username
string password
---
string state
需要注意的是Service的基本数据类型和常规的C++有些区别。例如在Service中int进一步细分成了int6、int32、int64,float细分成了float32和float64。所以在Service文件中如果写int是会报错的。
(3)编译自定义消息文件
a.手动方式
在编译自定义消息文件为头文件之前,需要修改login包的CMakeLists.txt文件,有以下需要修改的地方。
首先在自动生成的CMakeLists.txt文件中找到被注释掉的add_service_files(FILES Service1.srv),将其取消注释,并将自定义的srv文件名称填入,这行命令用于向项目中添加刚刚自定义的服务文件。
然后找到被注释掉的generate_messages(DEPENDENCIES std_msgs)将其取消注释,这行命令用于生成自定义的消息文件。
修改后的文件如下。

b.RoboWare方式
如果是在RoboWare里通过菜单新建的srv文件则无需修改,RoboWare已经自动修改好了CMakeLists.txt中的对应内容。
以上两种方式完成了CMake文件的修改之后即可以调用catkin_make进行编译了。在工作空间的根目录下catkin_make即可。之后我们可以在/root/catkin_ws/devel/include/login找到生成的三个user.h、userRequest.h、userResponse.h头文件了,后续调用它即可。
(4)创建server_login节点
a.手动方法
在包的根目录下新建一个src文件夹,然后在其中新建一个server.cpp文件,并在刚刚修改的CMakeLists.txt文件的末尾添加如下内容,用于添加可执行文件:
add_executable(server_login
src/server.cpp
)
add_dependencies(server_login ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(server_login
${catkin_LIBRARIES}
)
b.RoboWare方法
除了手动添加,也可以在RoboWare中自动添加。具体做法是在RoboWare的资源管理器中找到login,右键选择”新建Src文件夹”,在src文件夹上右键,然后选择”新建CPP源文件”,输入名称为server_login.cpp并回车,然后会弹出一个选择的对话框如下。
这是让你指定新建的这个cpp文件是作为一个库文件还是作为一个可执行文件,选择不同选项后,程序会自动帮你在CMakeLists.txt文件中填好相关内容。当然,如果不选也可以,自己手动填写就可以了。这里由于我们是希望生成一个可执行文件的,所以选择”加入到新的可执行文件中”即可。
(5)编写server_login节点代码
对于一个Service的服务端,需要以下几个步骤:初始化ROS节点(ros::init())、新建NodeHandle(ros::NodeHandle)、定义回调函数、构造ServiceServer(nh.advertiseService())、循环执行(ros::spin())。具体代码如下:
# include<ros/ros.h>
# include<login/user.h>
// 用于处理的回调函数,回调函数需要引用形式传入Request和Response,Response为返回数据
bool handle_function(login::user::Request &req,login::user::Response &res){
ROS_INFO("Request from %s with password %s",req.username.c_str(),req.password.c_str());
// 构建正确的登录信息
std::string name = "xuhui";
std::string passwd = "123456";
if(name == req.username and passwd == req.password){
res.state = "\nLogin success!\nWelcome my master " + req.username + ":)";
}else{
res.state = "\nLogin fail!\nYou are not my master. -_-";
}
return true;
}
int main(int argc, char *argv[])
{
// 初始化节点,指定节点名称
ros::init(argc, argv, "server_login");
// 创建节点句柄,用于控制节点
ros::NodeHandle nh;
// 创建服务端,并指定服务名称
ros::ServiceServer service = nh.advertiseService("login_info",handle_function);
// 循环执行
ros::spin();
return 0;
}
(6)创建client_login节点
a.手动方法
在src文件夹中新建一个client.cpp文件,并在CMakeLists.txt文件末尾添加如下内容,用于添加可执行文件:
add_executable(client_login
src/client.cpp
)
add_dependencies(client_login ${${PROJECT_NAME}_EXPORTED_TARGETS} ${catkin_EXPORTED_TARGETS})
target_link_libraries(client_login
${catkin_LIBRARIES}
)
b.RoboWare方法
除了手动添加,也可以在RoboWare中自动添加。具体做法是在RoboWare的资源管理器中找到login,在src文件夹上右键,然后选择”新建CPP源文件”,输入名称为client_login.cpp并回车。选择”加入到新的可执行文件中”即可。
(7)编写client_login节点代码
对于一个Service的客户端,需要以下几个步骤:初始化ROS节点(ros::init())、新建NodeHandle(ros::NodeHandle)、构造ServiceClient(nh.serviceClient())、发送消息(client.call())。具体代码如下:
# include<ros/ros.h>
# include<login/user.h>
int main(int argc, char *argv[])
{
// 初始化节点并指定节点名称
ros::init(argc, argv, "client_login");
// 创建句柄
ros::NodeHandle nh;
// 创建客户端并指定其发送的服务端名称
ros::ServiceClient client = nh.serviceClient<login::user>("login_info");
// 获取信息
std::string name;
std::string passwd;
std::cout<<"Input username:"<<std::endl;
std::cin>>name;
std::cout<<"Input password:"<<std::endl;
std::cin>>passwd;
// 新建一个消息,传入内容
login::user info;
info.request.username = name;
info.request.password = passwd;
// 注意这里call函数返回的就是在服务端定义的回调函数的返回值(true or false)
if (client.call(info))
{
ROS_INFO("%s",info.response.state.c_str());
}else{
ROS_ERROR("Failed to call service");
}
return 0;
}
(8)编译运行测试
最后在Catkin工作空间根目录下打开终端,使用catkin_make进行编译,编译成功后如下:

然后先输入roscore打开Master,然后在新终端中输入rosrun login server_login启动server_login节点,新终端中输入rosrun login client_login启动client_login节点。如下:

可以看到成功运行了刚刚写的两个节点,并且实现了服务消息的收发与处理。最后简单说一下,生成的两个节点的可执行文件放在了/root/catkin_ws/devel/lib/login里,直接在终端中运行这个可执行文件也是可以的(Master已经运行的情况下)。
3.Topic的Python实现
(1)新建Package
a.手动方法
在终端中输入如下内容即可创建一个Package。
cd catkin_ws/src/
catkin_create_pkg login rospy std_msgs message_generation message_runtime
第一行是切换到src目录下,第二行是在当前目录下创建一个叫login的包,并且包含rospy、std_msgs、message_generation、message_runtime依赖,后两个是用到Service的包都要包含的依赖。
b.RoboWare方法
打开RoboWare,在”资源管理器”中找到src文件夹,右键单击选择”新建ROS包”,输入名称即可创建Package。创建完成后右键Package选择”编辑依赖的ROS包列表”,输入rospy、std_msgs、message_generation、message_runtime(空格隔开)即可添加依赖。
(2)新建消息文件
与前面自定义消息文件内容步骤一模一样。
(3)编译消息文件
与前面编译自定义消息文件内容步骤一模一样。
(4)新建server_login节点文件
根据之前的习惯,src文件夹里一般放C/C++文件,Python代码一般放在scripts文件夹里。
a.手动方法
在package的根目录下创建一个scripts文件夹,然后新建一个server_login.py文件。由于Python是脚本,并不需要CMake编译,因此创建好文件就可以写代码了。
b.RoboWare方法
在包名上右键点击,选择”新建文件夹”,输入名称为scripts,在scripts文件夹上右键选择”新建文件”,输入server_login.py即可。
(5)编写server_login代码
参考之前笔记说过的rospy的API,代码如下。
# coding=utf-8
# 注意service模块的加载方式,from 包名.srv import *
# 其中srv指的是在包根目录下的srv文件夹,也即srv模块
import rospy
from login.srv import *
def handle_function(req):
# 回调函数传入的是user类型的请求,它的返回值直接就是userResponse类型的结果
rospy.loginfo("Request from %s with password %s",
req.username, req.password)
if req.username == "xuhui" and req.password == "123456":
return userResponse("\nLogin success!\nWelcome my master "+req.username+":)")
else:
return userResponse("\nLogin fail!\nYou are not my master. -_-")
def server_login():
# 初始化节点
rospy.init_node("server_login")
# 构造服务,指定服务名称,消息类型以及回调函数
ser = rospy.Service("login_info", user, handle_function)
rospy.loginfo("Ready to handle the request:")
# 循环执行
rospy.spin()
if __name__ == "__main__":
server_login()
(6)新建client_login节点文件
a.手动方法
在scripts文件夹下创建client_login.py即可。
b.RoboWare方法
在scripts文件夹上右键选择”新建文件”,输入client_login.py即可。
(7)编写client_login代码
Python代码如下:
# coding=utf-8
import rospy
from login.srv import *
def client_login():
# 初始化节点,指定名称
rospy.init_node("client_login")
# 等待指定的可用服务(阻塞)
rospy.wait_for_service("login_info")
# 输入信息
name = raw_input("Input username:\n")
passwd = raw_input("Input password:\n")
try:
# 构造客户端,指定服务端名称和传输的消息类型
client = rospy.ServiceProxy("login_info", user)
# 客户端发送消息,并返回结果
resp = client.call(name, passwd)
rospy.loginfo("%s", resp.state)
except rospy.ServiceException, e:
rospy.logwarn("Service call failed:%s" % e)
if __name__ == "__main__":
client_login()
(8)运行测试
最后在终端中输入roscore启动Master,在scripts目录下打开新终端然后分别输入python server_login.py启动服务节点,python client_login.py启动客户节点。运行结果如下:

4.Service的C++与Python实现比较
通过上面的介绍可以看到,ROS中Service的使用总体而言分为四步:新建包、自定义Service消息、编写代码以及编译运行。而且它和上一篇中介绍的Topic使用方法也十分相似。
对于C++和Python在实现起来整体步骤基本是相同的,只是由于API差异导致在代码编写这一步差别较大。其它步骤相同或有细微差别,如由于Python不需要编译因此没有catkin_make环节。具体对比如下表:
| 步骤 | C++ | Python |
|---|---|---|
| 创建Package | 依赖roscpp |
依赖rospy |
| 创建消息文件 | 相同 | 相同 |
| 编译消息文件 | 步骤相同,结果不同。生成xxx.h、xxxRequest.h、xxxResponse.h三个头文件,放在catkin_ws/devel/include/pkg_name |
步骤相同,结果不同。生成.py文件,放在catkin_ws/devel/lib/python2.7/dist-packages/pkg_name |
| 创建server_login节点 | 在src下 |
在scripts下 |
| 编写server_login节点代码 | 使用C++ API | 使用Python API |
| 创建client_login节点 | 在src下 |
在scripts下 |
| 编写client_login节点代码 | 使用C++ API | 使用Python API |
| 运行测试 | 运行前需要调用catkin_make编译 |
无需编译直接运行脚本 |
如果一个项目中既会用到C++也会用到Python,那么在一开始新建Package的时候就同时依赖roscpp和rospy,这样方便一些。此外在写代码的过程中注意区分Service名称、Service中传输的数据类型名称以及Service文件的名称。对于节点需要注意的是节点名称、CPP文件名称、以及CMake生成可执行文件名称的区分。
最后将所有代码上传到了Github,点击查看。
本文作者原创,未经许可不得转载,谢谢配合