(基于《ROS2机器人开发:从入门到实践》第4章及拓展知识)
服务(Service)是ROS2中基于“请求-响应”模式的双向通信机制,适用于需要明确反馈的交互场景(如任务执行、参数查询、算法计算等)。相比Python,C++实现的服务通信具有运行效率高、实时性强的特点,更适合机器人控制、传感器数据处理等性能敏感场景。
核心特征:
通信模式:客户端主动发送请求,服务端处理后返回响应(一对一交互)。接口定义:通过
.srv文件声明请求(Request)和响应(Response)数据结构,用
---分隔。生命周期:服务仅在被调用时激活,处理完成后释放资源,适合低频率、高复杂度的任务。
服务端和客户端需依赖ROS2核心库及自定义接口包,创建功能包命令如下:
# 创建C++功能包,依赖rclcpp(ROS2 C++库)和自定义接口包
ros2 pkg create demo_cpp_service --build-type ament_cmake
--dependencies rclcpp chapt4_interfaces turtlesim
依赖说明:
rclcpp:ROS2 C++核心库,提供节点、服务等基础功能。
chapt4_interfaces:自定义服务接口包(包含
.srv文件)。
turtlesim:示例中控制海龟运动的依赖(根据实际场景调整)。
package.xml声明编译和运行依赖,确保功能包能正确引用接口和ROS2库:
<?xml version="1.0"?>
<package format="3">
<name>demo_cpp_service</name>
<version>0.0.0</version>
<description>ROS2 C++服务示例</description>
<!-- 编译依赖 -->
<buildtool_depend>ament_cmake</buildtool_depend>
<build_depend>rclcpp</build_depend>
<build_depend>chapt4_interfaces</build_depend>
<build_depend>turtlesim</build_depend>
<!-- 运行依赖 -->
<exec_depend>rclcpp</exec_depend>
<exec_depend>chapt4_interfaces</exec_depend>
<exec_depend>turtlesim</exec_depend>
<export>
<build_type>ament_cmake</build_type>
</export>
</package>
CMakeLists.txt指定可执行文件、依赖库及安装规则(核心配置):
cmake_minimum_required(VERSION 3.8)
project(demo_cpp_service)
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
add_compile_options(-Wall -Wextra -Wpedantic) # 开启编译警告
endif()
# 查找依赖包
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(chapt4_interfaces REQUIRED)
find_package(turtlesim REQUIRED)
# 服务端可执行文件
add_executable(turtle_control src/turtle_control.cpp)
ament_target_dependencies(turtle_control
rclcpp
chapt4_interfaces
turtlesim
)
# 客户端可执行文件
add_executable(patrol_client src/patrol_client.cpp)
ament_target_dependencies(patrol_client
rclcpp
chapt4_interfaces
)
# 安装可执行文件到install目录
install(TARGETS
turtle_control
patrol_client
DESTINATION lib/${PROJECT_NAME}
)
ament_package()
服务端的核心逻辑是:创建服务对象 → 定义请求处理回调函数 → 处理请求并生成响应。
.srv文件)书中4.3节定义的巡逻服务接口
Patrol.srv:
# 请求:目标位置坐标
float32 target_x
float32 target_y
---
# 响应:处理结果
int8 SUCCESS=1 # 成功常量
int8 FAIL=0 # 失败常量
int8 result # 1表示成功,0表示失败
// src/turtle_control.cpp
#include "rclcpp/rclcpp.hpp"
#include "geometry_msgs/msg/twist.hpp" // 速度控制消息
#include "turtlesim/msg/pose.hpp" // 海龟位置消息
#include "chapt4_interfaces/srv/patrol.hpp" // 自定义巡逻服务接口
// 命名空间简化代码
using namespace std::placeholders;
using Patrol = chapt4_interfaces::srv::Patrol;
using Twist = geometry_msgs::msg::Twist;
using Pose = turtlesim::msg::Pose;
class TurtleController : public rclcpp::Node {
public:
TurtleController() : Node("turtle_control_node") {
// 1. 创建速度发布者(控制海龟运动)
vel_pub_ = this->create_publisher<Twist>("/turtle1/cmd_vel", 10);
// 2. 创建位置订阅者(获取海龟当前位置)
pose_sub_ = this->create_subscription<Pose>(
"/turtle1/pose", 10,
std::bind(&TurtleController::pose_callback, this, _1)
);
// 3. 创建服务对象(服务名称:"patrol",回调函数:patrol_callback)
patrol_service_ = this->create_service<Patrol>(
"patrol",
std::bind(&TurtleController::patrol_callback, this, _1, _2)
);
// 初始化目标位置和控制参数
target_x_ = 5.0;
target_y_ = 5.0;
kp_ = 1.0; // 比例控制系数
RCLCPP_INFO(this->get_logger(), "海龟巡逻服务已启动");
}
private:
// 位置回调:根据当前位置计算速度指令(闭环控制)
void pose_callback(const Pose::SharedPtr pose) {
// 计算当前位置与目标位置的误差
float dx = target_x_ - pose->x;
float dy = target_y_ - pose->y;
float distance = sqrt(dx*dx + dy*dy); // 距离误差
// 生成速度指令(比例控制)
auto cmd_vel = Twist();
if (distance > 0.1) { // 误差大于阈值时移动
cmd_vel.linear.x = kp_ * distance; // 线速度与距离成正比
cmd_vel.angular.z = 4.0 * atan2(dy, dx) - pose->theta; // 角速度控制转向
} else {
cmd_vel.linear.x = 0.0; // 误差小于阈值时停止
cmd_vel.angular.z = 0.0;
}
vel_pub_->publish(cmd_vel);
}
// 服务回调:处理客户端的巡逻请求
void patrol_callback(
const std::shared_ptr<Patrol::Request> request,
std::shared_ptr<Patrol::Response> response
) {
// 检查目标位置是否在海龟模拟器边界内(0~12.0)
if (request->target_x > 0 && request->target_x < 12.0 &&
request->target_y > 0 && request->target_y < 12.0) {
target_x_ = request->target_x; // 更新目标位置
target_y_ = request->target_y;
response->result = Patrol::Response::SUCCESS; // 响应成功
RCLCPP_INFO(this->get_logger(), "接受巡逻请求:(%.2f, %.2f)",
request->target_x, request->target_y);
} else {
response->result = Patrol::Response::FAIL; // 响应失败(目标越界)
RCLCPP_WARN(this->get_logger(), "目标位置越界:(%.2f, %.2f)",
request->target_x, request->target_y);
}
}
// 成员变量
rclcpp::Publisher<Twist>::SharedPtr vel_pub_; // 速度发布者
rclcpp::Subscription<Pose>::SharedPtr pose_sub_; // 位置订阅者
rclcpp::Service<Patrol>::SharedPtr patrol_service_; // 服务对象
float target_x_, target_y_; // 目标位置
float kp_; // 控制系数
};
// 主函数
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv); // 初始化ROS2
rclcpp::spin(std::make_shared<TurtleController>()); // 运行节点
rclcpp::shutdown(); // 退出时清理
return 0;
}
客户端的核心逻辑是:创建客户端对象 → 发送请求 → 等待并处理响应。
// src/patrol_client.cpp
#include "rclcpp/rclcpp.hpp"
#include "chapt4_interfaces/srv/patrol.hpp" // 巡逻服务接口
#include <chrono> // 时间相关
#include <cstdlib> // 随机数
#include <ctime> // 时间种子
using namespace std::chrono_literals;
using Patrol = chapt4_interfaces::srv::Patrol;
class PatrolClient : public rclcpp::Node {
public:
PatrolClient() : Node("patrol_client_node") {
// 1. 创建客户端对象(服务名称:"patrol",需与服务端一致)
client_ = this->create_client<Patrol>("patrol");
// 2. 创建定时器:每10秒发送一次巡逻请求
timer_ = this->create_wall_timer(
10s, // 10秒间隔
std::bind(&PatrolClient::send_request, this)
);
// 初始化随机数种子
std::srand(std::time(nullptr));
RCLCPP_INFO(this->get_logger(), "巡逻客户端已启动");
}
private:
// 发送请求函数
void send_request() {
// 等待服务端上线(超时1秒重试)
while (!client_->wait_for_service(1s)) {
if (!rclcpp::ok()) { // 若ROS2退出,终止等待
RCLCPP_ERROR(this->get_logger(), "等待服务被中断");
return;
}
RCLCPP_INFO(this->get_logger(), "等待服务端上线...");
}
// 生成随机目标位置(0~12.0,海龟模拟器边界)
auto request = std::make_shared<Patrol::Request>();
request->target_x = std::rand() % 12; // 随机x坐标
request->target_y = std::rand() % 12; // 随机y坐标
// 异步发送请求(非阻塞),并绑定响应回调
auto result_future = client_->async_send_request(
request,
std::bind(&PatrolClient::response_callback, this, _1)
);
}
// 响应回调函数:处理服务端返回的结果
void response_callback(rclcpp::Client<Patrol>::SharedFuture future) {
auto response = future.get(); // 获取响应
if (response->result == Patrol::Response::SUCCESS) {
RCLCPP_INFO(this->get_logger(), "巡逻请求已执行");
} else {
RCLCPP_WARN(this->get_logger(), "巡逻请求失败(目标越界)");
}
}
// 成员变量
rclcpp::Client<Patrol>::SharedPtr client_; // 客户端对象
rclcpp::TimerBase::SharedPtr timer_; // 定时器
};
// 主函数
int main(int argc, char * argv[]) {
rclcpp::init(argc, argv);
rclcpp::spin(std::make_shared<PatrolClient>());
rclcpp::shutdown();
return 0;
}
colcon build --packages-select demo_cpp_service
source install/setup.bash # 生效环境变量
# 终端1:启动海龟模拟器
ros2 run turtlesim turtlesim_node
# 终端2:启动服务端(海龟控制节点)
ros2 run demo_cpp_service turtle_control
# 终端3:启动客户端(随机巡逻请求)
ros2 run demo_cpp_service patrol_client
同步调用(
call()):阻塞当前线程,直到收到响应,适合简单场景:
// 同步调用示例(会阻塞,不推荐在回调中使用)
auto request = std::make_shared<Patrol::Request>();
request->target_x = 5.0;
request->target_y = 5.0;
auto response = client_->call(request); // 阻塞等待响应
异步调用(
async_send_request()):非阻塞,通过
SharedFuture获取结果,适合实时性要求高的场景:
// 异步调用(非阻塞)
auto result_future = client_->async_send_request(request);
// 可通过等待或回调处理结果(推荐回调方式)
ROS2服务端默认支持多客户端并发调用,回调函数会在独立线程中执行。若服务处理逻辑涉及共享资源(如全局变量),需通过互斥锁保证线程安全:
#include <mutex> // 互斥锁头文件
class TurtleController : public rclcpp::Node {
private:
std::mutex mutex_; // 互斥锁
float shared_data_; // 共享资源
void patrol_callback(
const std::shared_ptr<Patrol::Request> request,
std::shared_ptr<Patrol::Response> response
) {
std::lock_guard<std::mutex> lock(mutex_); // 自动加锁/解锁
shared_data_ = request->target_x; // 安全访问共享资源
// ...
}
};
客户端可设置请求超时时间,避免无限等待:
// 异步调用+超时控制
auto result_future = client_->async_send_request(request);
// 等待5秒,若超时则处理错误
if (result_future.wait_for(5s) == std::future_status::timeout) {
RCLCPP_ERROR(this->get_logger(), "请求超时");
} else {
auto response = result_future.get(); // 处理响应
}
服务端可通过参数动态调整行为(如控制系数),示例:
// 在服务端初始化时声明参数
this->declare_parameter("kp", 1.0); // 声明比例系数参数
kp_ = this->get_parameter("kp").as_double(); // 获取参数值
// 动态更新参数的回调
rclcpp::ParameterCallbackHandle::SharedPtr param_handle_;
param_handle_ = this->add_on_set_parameters_callback(
[this](const std::vector<rclcpp::Parameter> ¶ms) {
auto result = rcl_interfaces::msg::SetParametersResult();
result.successful = true;
for (const auto ¶m : params) {
if (param.get_name() == "kp") {
kp_ = param.as_double(); // 更新控制系数
RCLCPP_INFO(this->get_logger(), "kp更新为:%.2f", kp_);
}
}
return result;
}
);
ros2 service list:查看所有活跃服务。
ros2 service type /patrol:查看服务接口类型。
ros2 service call /patrol chapt4_interfaces/srv/Patrol "{target_x: 3.0, target_y: 4.0}":命令行调用服务。
rqt_graph:可视化服务与节点的连接关系。
ros2 doctor:检查服务通信是否存在异常。
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 编译报错“未定义接口类型” | 未在
CMakeLists.txt中添加接口依赖;接口包未编译 | 确保
ament_target_dependencies包含接口包;先编译接口包 |
| 服务调用失败“服务不存在” | 服务名称不匹配;服务端未启动;命名空间错误 | 检查服务名称和命名空间;用
ros2 service list确认服务是否存在 |
| 运行时崩溃“空指针访问” | 请求/响应对象未初始化;订阅者/发布者未正确创建 | 确保
create_service/
create_client返回非空指针;检查消息指针是否有效 |
| 并发调用导致数据错乱 | 共享资源未加锁 | 使用
std::mutex保护共享变量 |
C++开发ROS2服务端与客户端的核心流程可概括为:
定义服务接口(
.srv)并编译接口包。服务端:继承
rclcpp::Node,创建服务对象,实现请求处理回调函数,通过发布者/订阅者完成业务逻辑。客户端:创建客户端对象,异步发送请求,通过回调处理响应。
相比Python,C++需更多关注内存管理(如智能指针
SharedPtr)和线程安全(如互斥锁),但能提供更高的性能。结合参数动态配置和超时控制,可实现更健壮的服务通信逻辑,满足机器人系统的实时性需求。