4. 服务通信深入:自定义服务接口、同步与异步客户端、服务端多线程处理、超时与重试机制
服务通信,说白了就是ROS2里一种“你问我答”的机制。跟话题那种“你发我收”的广播模式不一样,服务是点对点的,有请求有响应。很多刚接触ROS2的朋友容易把服务和话题搞混,其实你想想看——话题适合持续流数据,比如传感器数据;服务适合一次性任务,比如“帮我算个路径”、“给我查个参数”。
我个人习惯,在需要确认结果的场景下,一定用服务。比如控制机械臂抓取,你不能发了指令就不管了,得等它告诉你“抓到了”或者“失败了”。这就是服务的价值。
4.1 自定义服务接口(.srv)
ROS2自带了一些标准服务,比如std_srvs/srv/Empty、std_srvs/srv/SetBool。但实际项目中,这些根本不够用。我做过一个自动驾驶项目,需要查询某个障碍物的详细信息,就得自己定义服务接口。
自定义.srv文件很简单。它由三部分组成:请求、分隔符、响应。
# 查询障碍物信息
# 请求
int32 obstacle_id
string query_type
---
# 响应
bool success
string obstacle_type
float32 distance
float32 velocity
string description
这里要注意几个坑:
- 字段类型:ROS2支持int32、float64、string、bool等基础类型,也支持数组和嵌套结构。
- 分隔符:三个短横线
---必须独占一行,前后不能有空格。我曾经因为多打了个空格,编译报错找了半小时。 - 包名约定:我建议把自定义服务接口放在单独的包里,比如
my_robot_interfaces。这样其他包引用起来方便,也符合ROS2的规范。
定义好之后,在CMakeLists.txt里加上:
rosidl_generate_interfaces(${PROJECT_NAME}
"srv/QueryObstacle.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>
编译之后,你就可以在代码里直接引用这个服务了。
4.2 同步客户端 vs 异步客户端
ROS2的客户端有两种调用方式:同步和异步。很多初学者搞不清区别,其实很简单。
同步客户端:调用后一直等着,直到拿到响应才继续往下走。适合简单场景,比如初始化时查个参数。
auto client = node->create_client<my_interfaces::srv::QueryObstacle>("query_obstacle");
auto request = std::make_shared<my_interfaces::srv::QueryObstacle::Request>();
request->obstacle_id = 101;
while (!client->wait_for_service(std::chrono::seconds(1))) {
RCLCPP_WARN(rclcpp::get_logger("rclcpp"), "等待服务中...");
}
auto result = client->async_send_request(request);
// 这里会阻塞,直到拿到结果
if (rclcpp::spin_until_future_complete(node, result) == rclcpp::FutureReturnCode::SUCCESS) {
auto response = result.get();
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "障碍物类型: %s", response->obstacle_type.c_str());
}
异步客户端:调用后不等待,直接返回。你可以继续做其他事,等响应回来了再处理。适合高实时性场景,比如控制循环里查询障碍物。
auto client = node->create_client<my_interfaces::srv::QueryObstacle>("query_obstacle");
auto request = std::make_shared<my_interfaces::srv::QueryObstacle::Request>();
request->obstacle_id = 101;
auto future = client->async_send_request(request,
[](rclcpp::Client<my_interfaces::srv::QueryObstacle>::SharedFuture future) {
auto response = future.get();
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "异步收到响应: %s", response->obstacle_type.c_str());
});
// 这里可以继续做其他事情
RCLCPP_INFO(rclcpp::get_logger("rclcpp"), "请求已发送,继续执行...");
4.3 服务端多线程处理
默认情况下,ROS2的服务端是单线程的。什么意思呢?就是一次只能处理一个请求。如果同时来了多个请求,后面的得排队。
这在低负载场景下没问题。但我在做多传感器融合时,多个模块同时请求障碍物信息,单线程就扛不住了。响应延迟从几毫秒飙升到几百毫秒。
解决办法是多线程服务端。ROS2的rclcpp支持通过CallbackGroup来实现。
// 创建多线程执行器
auto executor = std::make_shared<rclcpp::executors::MultiThreadedExecutor>();
// 创建回调组
auto callback_group = node->create_callback_group(
rclcpp::CallbackGroupType::MutuallyExclusive);
// 创建服务时指定回调组
auto service = node->create_service<my_interfaces::srv::QueryObstacle>(
"query_obstacle",
std::bind(&handle_query, this, std::placeholders::_1, std::placeholders::_2),
rmw_qos_profile_services_default,
callback_group);
executor->add_node(node);
executor->spin();
这里有个关键点:CallbackGroupType有两种:
| 类型 | 说明 | 适用场景 |
|---|---|---|
| MutuallyExclusive | 互斥,同一组内的回调不会同时执行 | 需要保护共享资源时 |
| Reentrant | 可重入,同一组内的回调可以同时执行 | 无状态服务,追求高并发 |
4.4 服务调用超时与重试机制
服务调用不是100%可靠的。网络抖动、服务端过载、节点崩溃,都可能导致调用失败。所以,超时和重试机制是必须的。
超时设置:ROS2的同步客户端支持超时参数。我一般设成2秒,超过就认为失败。
auto result = client->async_send_request(request);
auto status = rclcpp::spin_until_future_complete(node, result, std::chrono::seconds(2));
if (status == rclcpp::FutureReturnCode::TIMEOUT) {
RCLCPP_ERROR(rclcpp::get_logger("rclcpp"), "服务调用超时!");
// 处理超时逻辑
} else if (status == rclcpp::FutureReturnCode::SUCCESS) {
// 正常处理
}
重试机制:超时后不能直接放弃,得重试。我常用的策略是“指数退避”——第一次等1秒,第二次等2秒,第三次等4秒,最多重试3次。
int max_retries = 3;
int retry_delay = 1; // 初始延迟1秒
for (int i = 0; i < max_retries; ++i) {
auto result = client->async_send_request(request);
auto status = rclcpp::spin_until_future_complete(node, result, std::chrono::seconds(2));
if (status == rclcpp::FutureReturnCode::SUCCESS) {
// 成功,跳出循环
break;
}
RCLCPP_WARN(rclcpp::get_logger("rclcpp"),
"第%d次重试,等待%d秒...", i+1, retry_delay);
std::this_thread::sleep_for(std::chrono::seconds(retry_delay));
retry_delay *= 2; // 指数退避
}
4.5 知识体系总览
下面这张图总结了本章的核心内容,你可以对照着梳理思路:
嗯,以上就是服务通信深入的全部内容。从自定义接口到多线程处理,再到超时重试,每一步都是我在实际项目中踩过坑、总结出来的经验。你把这些吃透了,ROS2的服务通信这块基本就没什么能难住你的了。