4、服务通信实战:服务端节点创建、客户端节点创建、自定义服务接口、Gazebo中的模型控制服务
好,咱们今天聊点硬核的。服务通信,说白了就是ROS2里一种「你问我答」的机制。跟话题通信那种「广播式」的发布订阅不一样,服务通信是一对一的、有去有回的。你发一个请求,它回一个响应,干净利落。
我在做机器人底盘控制的时候,经常用服务来下发指令。比如让机器人「走到坐标(1,2)」,它走完了会告诉我「我到了」。这种场景用话题当然也能做,但服务更直接,也更符合直觉。
4.1 服务通信的核心概念
先理清几个关键角色:
- 服务端(Service Server):提供服务,接收请求,返回响应。说白了就是「被调用方」。
- 客户端(Service Client):调用服务,发送请求,等待响应。就是「调用方」。
- 服务接口(Service Interface):定义了请求和响应的数据结构。ROS2里用
.srv文件来描述。
你想想看,服务通信就像你去餐厅点餐。你是客户端,厨师是服务端。你点菜(请求),厨师做好端上来(响应)。就这么简单。
重要区别:话题通信是「发布-订阅」,一对多,数据流是单向的。服务通信是「请求-响应」,一对一,数据流是双向的。选哪个?看场景。如果你需要确认对方收到了并处理完了,用服务。如果只是广播数据,用话题。
4.2 自定义服务接口
ROS2自带了一些标准服务接口,比如std_srvs/srv/Empty、std_srvs/srv/SetBool。但实际项目中,我们经常需要自定义接口。嗯,这里要注意,自定义接口是ROS2开发的基本功。
我来演示一个控制Gazebo中模型的服务接口。假设我们要控制一个机器人模型的关节位置。
首先,在功能包下创建srv文件夹,然后新建SetJointPosition.srv文件:
# 请求部分
string joint_name # 关节名称
float64 target_position # 目标位置(弧度)
float64 duration # 运动持续时间(秒)
---
# 响应部分
bool success # 是否成功
string message # 状态信息
这里---是分隔符,上面是请求,下面是响应。我个人习惯把请求字段放在前面,响应字段放在后面,这样读起来很清晰。
然后修改CMakeLists.txt,添加:
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/SetJointPosition.srv"
)
再修改package.xml,确保有:
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
<member_of_group>rosidl_interface_packages</member_of_group>
编译后,你的自定义服务接口就生成了。在代码里可以直接#include使用。
小技巧:接口文件命名建议用驼峰式,比如SetJointPosition.srv。字段命名用下划线式,比如target_position。这是ROS社区的约定俗成,跟着走不会错。
4.3 服务端节点创建
服务端节点负责接收请求,执行任务,返回响应。我以控制Gazebo中模型关节为例,写一个服务端。
#include "rclcpp/rclcpp.hpp"
#include "your_package/srv/set_joint_position.hpp"
#include "gazebo_msgs/srv/set_joint_properties.hpp"
class JointPositionServer : public rclcpp::Node
{
public:
JointPositionServer() : Node("joint_position_server")
{
// 创建服务
service_ = this->create_service<your_package::srv::SetJointPosition>(
"set_joint_position",
std::bind(&JointPositionServer::handle_request, this, std::placeholders::_1, std::placeholders::_2)
);
RCLCPP_INFO(this->get_logger(), "服务端已启动,等待请求...");
}
private:
rclcpp::Service<your_package::srv::SetJointPosition>::SharedPtr service_;
void handle_request(
const std::shared_ptr<your_package::srv::SetJointPosition::Request> request,
std::shared_ptr<your_package::srv::SetJointPosition::Response> response)
{
RCLCPP_INFO(this->get_logger(), "收到请求: 关节=%s, 位置=%.2f, 时长=%.2f",
request->joint_name.c_str(),
request->target_position,
request->duration);
// 这里调用Gazebo服务来控制关节
// 实际项目中会调用 gazebo_msgs/srv/SetJointProperties
// 为了演示,我们直接模拟成功
response->success = true;
response->message = "关节 " + request->joint_name + " 已移动到 " +
std::to_string(request->target_position);
}
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<JointPositionServer>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
这段代码里,我创建了一个名为set_joint_position的服务。当客户端调用时,handle_request函数会被触发。我在项目中遇到过一个问题:服务端处理时间太长,导致客户端超时。解决办法是把耗时操作放到另一个线程里,或者用异步服务。
4.4 客户端节点创建
客户端负责发送请求,等待响应。写起来也很直接:
#include "rclcpp/rclcpp.hpp"
#include "your_package/srv/set_joint_position.hpp"
class JointPositionClient : public rclcpp::Node
{
public:
JointPositionClient() : Node("joint_position_client")
{
client_ = this->create_client<your_package::srv::SetJointPosition>("set_joint_position");
// 等待服务端上线
while (!client_->wait_for_service(std::chrono::seconds(1))) {
RCLCPP_WARN(this->get_logger(), "等待服务端上线...");
}
// 构造请求
auto request = std::make_shared<your_package::srv::SetJointPosition::Request>();
request->joint_name = "arm_joint";
request->target_position = 1.57; // 90度
request->duration = 2.0;
// 发送请求(异步方式)
auto future = client_->async_send_request(request);
// 等待响应
if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) ==
rclcpp::FutureReturnCode::SUCCESS)
{
auto response = future.get();
if (response->success) {
RCLCPP_INFO(this->get_logger(), "成功: %s", response->message.c_str());
} else {
RCLCPP_ERROR(this->get_logger(), "失败: %s", response->message.c_str());
}
} else {
RCLCPP_ERROR(this->get_logger(), "服务调用失败");
}
}
private:
rclcpp::Client<your_package::srv::SetJointPosition>::SharedPtr client_;
};
int main(int argc, char **argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<JointPositionClient>();
rclcpp::shutdown();
return 0;
}
这里我用了async_send_request异步调用。为什么不用同步?因为同步会阻塞整个节点,导致无法处理其他事情。异步调用配合spin_until_future_complete,既不会阻塞,又能拿到结果。
避坑指南:我曾经在客户端里忘了检查wait_for_service的返回值,结果服务端还没启动,客户端就直接发请求了,程序崩溃。记住:一定要先确认服务端在线,再发请求。
4.5 Gazebo中的模型控制服务
Gazebo本身提供了很多现成的服务,用于控制模型。常用的有:
| 服务名称 | 功能 | 接口类型 |
|---|---|---|
/gazebo/set_model_state |
设置模型的位置和姿态 | gazebo_msgs/srv/SetModelState |
/gazebo/apply_joint_effort |
对关节施加力矩 | gazebo_msgs/srv/ApplyJointEffort |
/gazebo/set_joint_properties |
设置关节属性(位置、速度等) | gazebo_msgs/srv/SetJointProperties |
/gazebo/spawn_urdf_model |
在仿真中生成URDF模型 | gazebo_msgs/srv/SpawnModel |
在实际项目中,我们通常不会直接调用这些服务,而是封装一层。比如我上面写的SetJointPosition服务,内部可以调用Gazebo的/gazebo/set_joint_properties来实现关节控制。这样做的好处是:
- 统一接口:不管底层是Gazebo还是真实硬件,对外接口不变
- 增加逻辑:可以在服务端里加限位检查、速度规划等
- 方便调试:可以在服务端里加日志,方便排查问题
我举个例子,在handle_request里调用Gazebo服务:
void handle_request(...)
{
// 创建Gazebo服务客户端
auto gazebo_client = this->create_client<gazebo_msgs::srv::SetJointProperties>(
"/gazebo/set_joint_properties");
// 等待Gazebo服务
if (!gazebo_client->wait_for_service(std::chrono::seconds(5))) {
response->success = false;
response->message = "Gazebo服务不可用";
return;
}
// 构造Gazebo请求
auto gazebo_request = std::make_shared<gazebo_msgs::srv::SetJointProperties::Request>();
gazebo_request->joint_name = request->joint_name;
// ... 设置其他参数
// 调用Gazebo服务
auto future = gazebo_client->async_send_request(gazebo_request);
// ... 处理响应
}
你看,服务端内部又调用了另一个服务。这就是服务链的概念。在复杂系统中,服务可以层层嵌套,形成调用链。
4.6 调试与测试
写完代码怎么测?我推荐几个方法:
- 命令行测试:用
ros2 service list查看所有服务,用ros2 service call直接调用服务。比如:
ros2 service call /set_joint_position your_package/srv/SetJointPosition "{joint_name: 'arm_joint', target_position: 1.57, duration: 2.0}" - rqt工具:启动
rqt,选择Services插件,图形化调用服务。 - 日志观察:在服务端和客户端里加
RCLCPP_INFO,观察调用流程。
调试心得:我习惯在服务端里打印请求参数和响应结果,在客户端里打印调用耗时。这样一眼就能看出是服务端处理慢了,还是网络延迟了。
4.7 本章小结
服务通信是ROS2里非常实用的机制。它比话题通信更直接,适合那些需要确认结果的场景。自定义服务接口让你可以定义自己的数据结构,非常灵活。Gazebo提供的服务接口,让我们可以方便地控制仿真模型。
嗯,最后说一句:服务通信虽然好用,但别滥用。如果某个操作需要频繁调用(比如每10ms一次),那还是用话题吧。服务适合那些「偶尔调用、需要确认」的场景。
可可精品资料库