ROS2进阶:服务通信、自定义SRV接口、参数服务器与基于服务的参数通信详解

在前两篇博客中,我们彻底掌握了 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. 三者使用场景终极区分

  • 话题:高速、持续、实时数据流(雷达、图像、速度)

  • 服务:单次、问答、触发式任务(开关、查询、执行动作)

  • 参数:静态配置、动态调参(底层依赖服务通信)

七、总结

  1. 服务通信是双向同步问答机制,适配单次指令交互场景,和话题异步广播形成互补;

  2. 自定义 .srv 接口是ROS2项目开发的核心能力,可灵活定义双向数据格式;

  3. 参数服务器无独立通信机制,所有读写操作均基于ROS2内置服务实现,是封装简化后的服务通信;

  4. 话题、服务、参数三者构成ROS2完整通信体系,掌握三者区别与底层原理,才算真正入门ROS2核心开发。

相关推荐
Wang's Blog1 小时前
Java 项目实战: 交友项目 - MongoDB 入门:体系结构和命令行操作
java·mongodb·交友
小溪学编程1 小时前
将 Bean 放入 Spring 容器中的方式有哪些?
java·后端·spring
java_nnnn1 小时前
LeetCode刷题滑动窗口(水果成篮+找到字符串中所有字母的异位词)Java
java·开发语言·leetcode
用户3721574261351 小时前
Java 读取、修改和清空 PDF 文档属性(元数据)
java
SL_staff1 小时前
从‘改一行代码等三天’到‘改完即生效’:实施工程师的逻辑引擎避坑手记
java·产品·全栈
qetfw1 小时前
Bash 字符串处理:参数展开、序列与脚本执行方式
linux·开发语言·chrome·bash
小羊没烦恼!1 小时前
异步编程:线程概述及使用
java·开发语言·mysql·算法·c#
hhwyqwqhhwy1 小时前
设备树之 provider 和 consumer
java·linux·前端
\光辉岁月/1 小时前
9.日期时间类
java·date·localdatetime·日期类