在前两篇博客中,我们彻底掌握了 ROS2 节点工程架构 和 话题(Topic)单向通信。
话题通信的特点是异步、单向、持续广播 ,但机器人开发中,有大量场景需要 一问一答、执行动作、需要返回结果,比如:开关设备、获取设备状态、修改运行参数、触发单次任务。
针对这类场景,ROS2 提供了 服务(Service)通信 。更关键的是:ROS2 参数服务器的底层,完全基于服务通信实现。
本文将从零讲解服务通信机制、手把手实现标准服务C++客户端/服务端、自定义SRV服务接口、深度解析参数服务器,最后讲透基于服务的参数通信底层原理,补齐ROS2三大核心通信(话题、服务、参数)体系。
一、ROS2 服务通信核心原理
1. 什么是服务通信?
服务通信是 ROS2 中同步、双向、一问一答的通信模型,严格分为两端:
-
服务端(Server):被动等待,常驻后台,提供服务能力
-
客户端(Client):主动发起请求,触发一次任务
一次完整服务流程:客户端发请求 → 服务端接收处理 → 服务端返回响应 → 单次通信结束。
2. 服务 vs 话题(核心区别必记)
| 特性 | 话题 Topic | 服务 Service |
|---|---|---|
| 通信方式 | 异步、单向广播 | 同步、双向问答 |
| 使用场景 | 持续数据流(雷达、图像、速度) | 单次任务、指令交互(开关、查询、修改) |
| 是否阻塞 | 非阻塞 | 客户端等待响应,短暂阻塞 |
| 通信模型 | 多对多 | 一对一 |
3. 服务通信文件格式
话题使用 .msg 单向消息文件;.srv 服务使用 双向接口文件。
srv 文件固定格式:请求数据 --- 响应数据,中间用三条横线分隔,天然区分输入和输出。
二、标准服务 C++ 实操(官方接口)
我们使用 ROS2 官方标准服务 std_srvs/srv/SetBool,实现「客户端发布尔指令、服务端接收并返回执行结果」。
1. 创建功能包
cd ~/ros2_ws/src
ros2 pkg create --build-type ament_cmake --dependencies rclcpp std_srvs ros_service_demo
2. 编写服务端节点(service_server.cpp)
cpp
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/set_bool.hpp"
class ServiceServer : public rclcpp::Node
{
public:
ServiceServer() : Node("service_server_node")
{
// 创建服务:服务名 /set_bool_server,绑定回调函数
server_ = this->create_service<std_srvs::srv::SetBool>(
"/set_bool_server",
std::bind(&ServiceServer::server_callback, this, std::placeholders::_1, std::placeholders::_2)
);
RCLCPP_INFO(this->get_logger(), "服务端启动成功,等待客户端请求...");
}
private:
// 服务回调:请求+响应双参数
void server_callback(
const std::shared_ptr<std_srvs::srv::SetBool::Request> request,
std::shared_ptr<std_srvs::srv::SetBool::Response> response)
{
// 处理请求逻辑
if (request->data)
{
RCLCPP_INFO(this->get_logger(), "收到开启指令,执行开启操作");
response->success = true;
response->message = "设备开启成功";
}
else
{
RCLCPP_INFO(this->get_logger(), "收到关闭指令,执行关闭操作");
response->success = true;
response->message = "设备关闭成功";
}
}
rclcpp::Service<std_srvs::srv::SetBool>::SharedPtr server_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<ServiceServer>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
3. 编写客户端节点(service_client.cpp)
cpp
#include "rclcpp/rclcpp.hpp"
#include "std_srvs/srv/set_bool.hpp"
class ServiceClient : public rclcpp::Node
{
public:
ServiceClient() : Node("service_client_node")
{
client_ = this->create_client<std_srvs::srv::SetBool>("/set_bool_server");
// 等待服务端上线
while (!client_->wait_for_service(std::chrono::seconds(1)))
{
RCLCPP_WARN(this->get_logger(), "等待服务端启动...");
}
RCLCPP_INFO(this->get_logger(), "服务端已连接,发起请求");
// 构造请求数据
auto request = std::make_shared<std_srvs::srv::SetBool::Request>();
request->data = true;
// 异步发送请求并绑定回调
client_->async_send_request(request, std::bind(&ServiceClient::client_callback, this, std::placeholders::_1));
}
private:
// 接收服务端响应
void client_callback(rclcpp::Client<std_srvs::srv::SetBool>::SharedFuture future)
{
auto result = future.get();
RCLCPP_INFO(this->get_logger(), "服务响应结果:%s", result->message.c_str());
}
rclcpp::Client<std_srvs::srv::SetBool>::SharedPtr client_;
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<ServiceClient>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
4. 配置编译规则 CMakeLists.txt
cpp
# 服务端编译
add_executable(service_server src/service_server.cpp)
ament_target_dependencies(service_server rclcpp std_srvs)
# 客户端编译
add_executable(service_client src/service_client.cpp)
ament_target_dependencies(service_client rclcpp std_srvs)
# 安装可执行文件
install(TARGETS
service_server
service_client
DESTINATION lib/${PROJECT_NAME}
)
5. 编译运行测试
cpp
cd ~/ros2_ws
colcon build --packages-select ros_service_demo
source install/setup.bash
# 终端1:启动服务端
ros2 run ros_service_demo service_server
# 终端2:启动客户端
ros2 run ros_service_demo service_client
实现效果:客户端发请求,服务端处理后返回结果,双向通信完成。
三、自定义服务接口(.srv)全流程实操
官方服务接口无法满足自定义业务需求,ROS2 支持手动编写 .srv 文件,生成专属双向服务接口,是项目开发必备技能。
1. 创建srv目录与自定义接口文件
cpp
cd ~/ros2_ws/src/ros_service_demo
mkdir srv
touch srv/AddTwoInts.srv
编写自定义服务:两数相加,请求两个整数,返回求和结果
cpp
# 请求数据(客户端传给服务端)
int64 a
int64 b
---
# 响应数据(服务端返回客户端)
int64 sum
2. 修改配置文件(核心!否则无法生成接口)
第一步:package.xml 添加依赖
cpp
<depend>sensor_msgs</depend>
<build_depend>rosidl_default_generators</build_depend>
<exec_depend>rosidl_default_runtime</exec_depend>
第二步:CMakeLists.txt 添加接口编译规则
cpp
# 添加自定义srv编译
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/AddTwoInts.srv"
)
ament_export_dependencies(rosidl_default_runtime)
3. 编译生效
cpp
colcon build --packages-select ros_service_demo
source install/setup.bash
# 验证自定义服务
ros2 interface show ros_service_demo/srv/AddTwoInts
打印出接口内容即代表自定义服务创建成功,可直接在代码中调用。
四、ROS2 参数服务器核心原理
1. 什么是参数?
参数是 ROS2 节点的全局配置变量,用于存储静态配置、阈值、参数系数,无需修改代码即可修改节点运行行为,常用于:帧率、阈值、PID参数、设备ID、开关配置。
参数特点:键值对存储、全局共享、动态修改、持久化配置。
2. 参数核心操作
cpp
# 查看节点参数列表
ros2 param list /节点名
# 获取参数值
ros2 param get /节点名 参数名
# 动态设置参数
ros2 param set /节点名 参数名 参数值
# 保存参数
ros2 param save /节点名 xxx.yaml
# 加载参数
ros2 param load /节点名 xxx.yaml
五、重点:基于服务的参数通信(底层核心)
所有ROS2参数操作,底层全部是服务通信!
这是新手最容易忽略、面试高频的核心知识点:ROS2没有独立的参数通信机制,参数读写、修改、保存,全部封装为内置服务。
1. 底层原理
每个启动的ROS2节点,都会自动自带一批参数服务端,对外提供服务:
-
/xxx/get_parameters:获取参数服务 -
/xxx/set_parameters:设置参数服务 -
/xxx/list_parameters:查询参数服务
我们在终端执行的 ros2 param get/set 命令,本质是创建一个临时客户端,调用节点内置的参数服务,完成数据交互。
2. 验证参数基于服务通信
启动任意节点,查看服务列表:
cpp
ros2 run turtlesim turtlesim_node
ros2 service list | grep param
可以清晰看到所有参数相关服务,直接证明:参数通信 = 封装后的服务通信。
3. C++ 代码读写参数(基于服务封装)
ROS2 封装了简易接口,底层自动调用参数服务,无需手动写服务客户端:
cpp
#include "rclcpp/rclcpp.hpp"
class ParamNode : public rclcpp::Node
{
public:
ParamNode() : Node("param_node")
{
// 1. 声明参数(默认值)
this->declare_parameter<int>("camera_fps", 30);
this->declare_parameter<std::string>("device_name", "robot_camera");
// 2. 读取参数(底层调用get_parameters服务)
int fps = this->get_parameter("camera_fps").as_int();
std::string name = this->get_parameter("device_name").as_string();
RCLCPP_INFO(this->get_logger(), "读取参数:帧率=%d, 设备名=%s", fps, name.c_str());
// 3. 设置参数(底层调用set_parameters服务)
this->set_parameter(rclcpp::Parameter("camera_fps", 60));
RCLCPP_INFO(this->get_logger(), "参数修改成功,新帧率=%d", this->get_parameter("camera_fps").as_int());
}
};
int main(int argc, char ** argv)
{
rclcpp::init(argc, argv);
auto node = std::make_shared<ParamNode>();
rclcpp::spin(node);
rclcpp::shutdown();
return 0;
}
六、服务与参数通信 工程最佳实践
1. 服务通信最佳实践
-
服务名统一小写下划线命名,全局唯一,禁止重名
-
服务回调函数禁止耗时阻塞,避免卡死服务端
-
客户端必须先判断服务是否在线,防止请求失败崩溃
-
自定义srv接口语义清晰,请求、响应字段注释完整,便于团队协作
2. 参数通信最佳实践
-
所有可配置参数必须 先声明、后使用,规范代码逻辑
-
高频变化数据不用参数传输,参数只用于配置,数据流用话题
-
批量参数统一写入yaml配置文件,开机加载,不硬编码
-
禁止频繁动态修改参数,参数修改会触发服务通信,占用资源
3. 三者使用场景终极区分
-
话题:高速、持续、实时数据流(雷达、图像、速度)
-
服务:单次、问答、触发式任务(开关、查询、执行动作)
-
参数:静态配置、动态调参(底层依赖服务通信)
七、总结
-
服务通信是双向同步问答机制,适配单次指令交互场景,和话题异步广播形成互补;
-
自定义
.srv接口是ROS2项目开发的核心能力,可灵活定义双向数据格式; -
参数服务器无独立通信机制,所有读写操作均基于ROS2内置服务实现,是封装简化后的服务通信;
-
话题、服务、参数三者构成ROS2完整通信体系,掌握三者区别与底层原理,才算真正入门ROS2核心开发。