在ROS2中,服务(Service)是一种重要的通信机制,它实现了节点之间的同步请求-响应模式。想象一下你在餐厅点餐的场景:你(客户端)向服务员(服务端)提出点餐请求,服务员处理你的请求并给你回复。这就是服务通信的基本模式。
服务通信涉及两个角色:
很多初学者容易混淆服务和话题,让我们来看看它们的主要区别:
话题(Topic)通信特点:
服务(Service)通信特点:
服务特别适合以下应用场景:
服务通信遵循以下步骤:
每个服务都需要定义其接口,这就像制定API规范一样。服务接口通过.srv
文件定义,包含两部分:
# 请求部分(Request)
# 定义客户端向服务端发送的数据格式
请求字段1 类型
请求字段2 类型
---
# 响应部分(Response)
# 定义服务端向客户端返回的数据格式
响应字段1 类型
响应字段2 类型
示例 - 两数相加服务 AddTwoInts.srv
:
# 请求:客户端发送两个整数
int64 a
int64 b
---
# 响应:服务端返回计算结果
int64 sum
这个定义表示:客户端发送两个64位整数a和b,服务端计算它们的和并返回结果sum。
ROS2提供了一些预定义的服务类型,新手可以直接使用:
// 基础服务类型
std_srvs/srv/Empty // 空服务:无请求参数,无响应数据
std_srvs/srv/SetBool // 布尔设置:发送布尔值,返回成功状态和消息
std_srvs/srv/Trigger // 触发服务:无参数触发,返回成功状态和消息
// 示例服务类型
example_interfaces/srv/AddTwoInts // 两数相加
example_interfaces/srv/SetBool // 设置布尔参数
创建服务端需要以下几个关键步骤:
步骤1:包含必要的头文件
#include "rclcpp/rclcpp.hpp" // ROS2 C++基础库
#include "example_interfaces/srv/add_two_ints.hpp" // 服务接口定义
#include // 智能指针支持
步骤2:创建服务端节点类
class AddTwoIntsServer : public rclcpp::Node
{
public:
AddTwoIntsServer() : Node("add_two_ints_server")
{
// 在构造函数中创建服务
service_ = this->create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints", // 服务名称
std::bind(&AddTwoIntsServer::handle_service, this,
std::placeholders::_1, std::placeholders::_2) // 回调函数
);
RCLCPP_INFO(this->get_logger(), "加法服务端已启动,等待客户端请求...");
}
private:
// 处理服务请求的回调函数
void handle_service(
const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> request,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> response)
{
// 执行加法运算
response->sum = request->a + request->b;
// 输出日志信息,便于调试
RCLCPP_INFO(this->get_logger(),
"收到请求: %ld + %ld,计算结果: %ld",
request->a, request->b, response->sum);
}
// 服务对象
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service_;
};
步骤3:编写主函数
int main(int argc, char** argv)
{
// 初始化ROS2
rclcpp::init(argc, argv);
// 创建服务端节点
auto node = std::make_shared<AddTwoIntsServer>();
// 进入事件循环,等待和处理请求
rclcpp::spin(node);
// 清理资源
rclcpp::shutdown();
return 0;
}
客户端的实现稍微复杂一些,因为需要处理异步通信:
步骤1:创建客户端节点类
class AddTwoIntsClient : public rclcpp::Node
{
public:
AddTwoIntsClient() : Node("add_two_ints_client")
{
// 创建服务客户端
client_ = this->create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
}
void send_request(int64_t a, int64_t b)
{
// 步骤1:等待服务变为可用状态
// 这很重要!如果服务端还没启动,客户端会一直等待
while (!client_->wait_for_service(std::chrono::seconds(1))) {
if (!rclcpp::ok()) {
RCLCPP_ERROR(this->get_logger(), "程序被中断退出");
return;
}
RCLCPP_INFO(this->get_logger(), "等待服务 'add_two_ints' 可用...");
}
// 步骤2:创建并填充请求消息
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = a;
request->b = b;
RCLCPP_INFO(this->get_logger(), "发送请求: %ld + %ld", a, b);
// 步骤3:异步发送请求
auto future = client_->async_send_request(request);
// 步骤4:等待响应
if (rclcpp::spin_until_future_complete(this->get_node_base_interface(), future) ==
rclcpp::FutureReturnCode::SUCCESS) {
// 请求成功,获取响应
auto response = future.get();
RCLCPP_INFO(this->get_logger(), "收到响应: %ld + %ld = %ld",
a, b, response->sum);
} else {
// 请求失败
RCLCPP_ERROR(this->get_logger(), "服务调用失败或超时");
}
}
private:
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
};
步骤2:主函数实现
int main(int argc, char** argv)
{
rclcpp::init(argc, argv);
// 检查命令行参数
if (argc != 3) {
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),
"用法: ros2 run package_name client X Y");
RCLCPP_INFO(rclcpp::get_logger("rclcpp"),
"例如: ros2 run package_name client 10 20");
return 1;
}
// 创建客户端并发送请求
auto node = std::make_shared<AddTwoIntsClient>();
node->send_request(atoll(argv[1]), atoll(argv[2]));
rclcpp::shutdown();
return 0;
}
上面的例子使用了异步调用。让我解释一下同步和异步的区别:
异步调用(推荐):
同步调用(简单但不推荐):
void sync_call_example(int64_t a, int64_t b)
{
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = a;
request->b = b;
// 同步调用 - 程序会在这里阻塞等待
auto future_result = client_->async_send_request(request);
auto status = future_result.wait_for(std::chrono::seconds(5)); // 最多等待5秒
if (status == std::future_status::ready) {
auto response = future_result.get();
RCLCPP_INFO(this->get_logger(), "结果: %ld", response->sum);
} else {
RCLCPP_ERROR(this->get_logger(), "服务调用超时");
}
}
Python的实现相对更简洁,但概念是相同的。
步骤1:导入必要的模块
#!/usr/bin/env python3
import rclpy # ROS2 Python基础库
from rclpy.node import Node # 节点基类
from example_interfaces.srv import AddTwoInts # 服务接口
步骤2:创建服务端类
class AddTwoIntsServer(Node):
def __init__(self):
# 调用父类构造函数,设置节点名称
super().__init__('add_two_ints_server')
# 创建服务
# 参数说明:
# - AddTwoInts: 服务类型
# - 'add_two_ints': 服务名称
# - self.handle_service: 处理请求的回调函数
self.srv = self.create_service(
AddTwoInts,
'add_two_ints',
self.handle_service
)
self.get_logger().info('加法服务端已启动,正在等待客户端请求...')
def handle_service(self, request, response):
"""
处理服务请求的回调函数
参数:
request: 客户端发送的请求数据
response: 需要填充并返回给客户端的响应数据
返回:
response: 填充完毕的响应数据
"""
# 执行加法计算
response.sum = request.a + request.b
# 记录日志,便于调试和监控
self.get_logger().info(
f'收到请求: {request.a} + {request.b},计算结果: {response.sum}'
)
# 必须返回response对象
return response
步骤3:主函数
def main(args=None):
# 初始化ROS2 Python客户端库
rclpy.init(args=args)
# 创建服务端节点
server = AddTwoIntsServer()
try:
# 进入事件循环,持续监听和处理请求
rclpy.spin(server)
except KeyboardInterrupt:
# 用户按Ctrl+C时优雅退出
print('\n程序被用户中断')
finally:
# 清理资源
server.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
步骤1:创建客户端类
class AddTwoIntsClient(Node):
def __init__(self):
super().__init__('add_two_ints_client')
# 创建服务客户端
self.client = self.create_client(AddTwoInts, 'add_two_ints')
# 等待服务变为可用
# 这是一个重要步骤!确保服务端已经启动
timeout_counter = 0
while not self.client.wait_for_service(timeout_sec=1.0):
timeout_counter += 1
self.get_logger().info(f'等待服务可用... ({timeout_counter}秒)')
# 避免无限等待
if timeout_counter > 10:
self.get_logger().error('服务长时间不可用,请检查服务端是否启动')
return
self.get_logger().info('服务已连接,可以发送请求')
def send_request(self, a, b):
"""
发送服务请求
参数:
a, b: 要相加的两个数字
返回:
Future对象,用于异步获取响应
"""
# 创建请求对象
request = AddTwoInts.Request()
request.a = a
request.b = b
self.get_logger().info(f'发送请求: {a} + {b}')
# 异步发送请求
future = self.client.call_async(request)
return future
步骤2:主函数实现
import sys # 用于处理命令行参数
def main(args=None):
rclpy.init(args=args)
# 检查命令行参数
if len(sys.argv) != 3:
print('错误:参数数量不正确')
print('用法: ros2 run package_name client X Y')
print('例如: ros2 run package_name client 10 20')
return
try:
# 将命令行参数转换为整数
num1 = int(sys.argv[1])
num2 = int(sys.argv[2])
except ValueError:
print('错误:参数必须是整数')
return
# 创建客户端
client = AddTwoIntsClient()
try:
# 发送请求
future = client.send_request(num1, num2)
# 等待响应
rclpy.spin_until_future_complete(client, future)
# 检查响应结果
if future.result() is not None:
response = future.result()
client.get_logger().info(
f'收到响应: {num1} + {num2} = {response.sum}'
)
else:
client.get_logger().error('服务调用失败,未收到响应')
except KeyboardInterrupt:
print('\n程序被用户中断')
finally:
client.destroy_node()
rclpy.shutdown()
if __name__ == '__main__':
main()
Python也支持同步调用,但需要小心使用:
def sync_call_service(self, a, b):
"""
同步服务调用示例
注意:这会阻塞程序直到收到响应或超时
"""
request = AddTwoInts.Request()
request.a = a
request.b = b
try:
# 同步调用,最多等待5秒
response = self.client.call(request, timeout_sec=5.0)
self.get_logger().info(f'同步调用结果: {response.sum}')
return response
except Exception as e:
self.get_logger().error(f'服务调用失败: {e}')
return None
CMakeLists.txt 核心修改:
# 1. 在find_package中添加服务依赖
find_package(example_interfaces REQUIRED)
# 2. 编译可执行文件时链接服务接口
add_executable(server src/server.cpp)
ament_target_dependencies(server rclcpp example_interfaces) # 注意这里添加example_interfaces
add_executable(client src/client.cpp)
ament_target_dependencies(client rclcpp example_interfaces) # 注意这里添加example_interfaces
package.xml 核心修改:
<depend>example_interfacesdepend>
setup.py 核心修改:
entry_points={
'console_scripts': [
# 这里定义了可以通过ros2 run运行的命令
'server = service_example_py.server:main',
'client = service_example_py.client:main',
],
},
package.xml 核心修改:
<exec_depend>rclpyexec_depend>
<exec_depend>example_interfacesexec_depend>
C++项目结构:
service_example_cpp/
├── CMakeLists.txt
├── package.xml
└── src/
├── server.cpp
└── client.cpp
Python项目结构:
service_example_py/
├── package.xml
├── setup.py
├── setup.cfg
├── resource/
│ └── service_example_py
└── service_example_py/
├── __init__.py
├── server.py
└── client.py
第一步:创建工作空间
mkdir -p ~/ros2_ws/src
cd ~/ros2_ws/src
第二步:创建包
# C++包
ros2 pkg create --build-type ament_cmake service_example_cpp --dependencies rclcpp example_interfaces
# Python包
ros2 pkg create --build-type ament_python service_example_py --dependencies rclpy example_interfaces
第三步:编写代码
将前面的服务端和客户端代码分别放入对应的文件中。
第四步:编译
cd ~/ros2_ws
colcon build --packages-select service_example_cpp service_example_py
第五步:测试运行
# 激活环境
source install/setup.bash
# 第一个终端运行服务端
ros2 run service_example_cpp server
# 第二个终端运行客户端
ros2 run service_example_cpp client 10 20
查看服务状态:
# 列出所有可用服务
ros2 service list
# 查看服务类型
ros2 service type /add_two_ints
# 查看服务接口定义
ros2 interface show example_interfaces/srv/AddTwoInts
# 直接调用服务进行测试
ros2 service call /add_two_ints example_interfaces/srv/AddTwoInts "{a: 15, b: 25}"
查看节点信息:
# 列出运行中的节点
ros2 node list
# 查看特定节点信息
ros2 node info /add_two_ints_server
# 查看计算图
rqt_graph
问题1:找不到服务接口
fatal error: example_interfaces/srv/add_two_ints.hpp: No such file or directory
解决方案:
find_package(example_interfaces REQUIRED)
ament_target_dependencies
中添加了example_interfaces
rosdep install --from-paths src --ignore-src -r -y
安装依赖问题2:Python模块找不到
ModuleNotFoundError: No module named 'example_interfaces'
解决方案:
example_interfaces
colcon build && source install/setup.bash
问题1:服务不可用
[INFO]: 等待服务 'add_two_ints' 可用...
解决方案:
ros2 node list
ros2 service list
ROS_DOMAIN_ID
环境变量问题2:客户端超时
[ERROR]: 服务调用失败或超时
解决方案:
日志输出技巧:
// C++中设置日志级别
RCLCPP_DEBUG(this->get_logger(), "详细调试信息: a=%ld, b=%ld", a, b);
RCLCPP_INFO(this->get_logger(), "正常信息");
RCLCPP_WARN(this->get_logger(), "警告信息");
RCLCPP_ERROR(this->get_logger(), "错误信息");
# Python中的日志输出
self.get_logger().debug(f'详细调试信息: a={a}, b={b}')
self.get_logger().info('正常信息')
self.get_logger().warn('警告信息')
self.get_logger().error('错误信息')
运行时设置日志级别:
# 设置DEBUG级别查看详细信息
ros2 run service_example_cpp server --ros-args --log-level DEBUG
# 查看所有日志输出
ros2 topic echo /rosout
服务端错误处理:
void handle_service(const Request::SharedPtr request, Response::SharedPtr response)
{
try {
// 输入验证
if (request->a > 1000000 || request->b > 1000000) {
RCLCPP_WARN(this->get_logger(), "输入值过大,限制在合理范围内");
response->sum = 0; // 或者返回错误码
return;
}
// 执行计算
response->sum = request->a + request->b;
RCLCPP_INFO(this->get_logger(), "成功处理请求: %ld + %ld = %ld",
request->a, request->b, response->sum);
} catch (const std::exception& e) {
RCLCPP_ERROR(this->get_logger(), "处理服务时发生异常: %s", e.what());
response->sum = 0; // 设置默认值
}
}
客户端超时处理:
def call_service_with_timeout(self, a, b, timeout_seconds=5.0):
"""带超时处理的服务调用"""
request = AddTwoInts.Request()
request.a = a
request.b = b
future = self.client.call_async(request)
# 使用自定义超时
start_time = time.time()
while rclpy.ok():
rclpy.spin_once(self, timeout_sec=0.1)
if future.done():
result = future.result()
if result is not None:
return result
else:
self.get_logger().error('服务返回空结果')
return None
if time.time() - start_time > timeout_seconds:
self.get_logger().error(f'服务调用超时({timeout_seconds}秒)')
return None
return None
减少内存分配:
class OptimizedServer : public rclcpp::Node
{
private:
// 预分配响应对象,避免频繁创建
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> cached_response_;
public:
OptimizedServer() : Node("optimized_server")
{
cached_response_ = std::make_shared<example_interfaces::srv::AddTwoInts::Response>();
// ... 其他初始化代码
}
};
并发处理:
// 使用回调组支持并发处理多个请求
class ConcurrentServer : public rclcpp::Node
{
public:
ConcurrentServer() : Node("concurrent_server")
{
// 创建可重入的回调组
callback_group_ = this->create_callback_group(
rclcpp::CallbackGroupType::Reentrant);
service_ = this->create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints",
std::bind(&ConcurrentServer::handle_service, this, _1, _2),
rmw_qos_profile_services_default,
callback_group_);
}
private:
rclcpp::CallbackGroup::SharedPtr callback_group_;
};
状态查询服务:
// 查询机器人状态的服务示例
void handle_status_query(
const std_srvs::srv::Trigger::Request::SharedPtr request,
std_srvs::srv::Trigger::Response::SharedPtr response)
{
// 获取当前状态
std::string status = get_robot_status();
response->success = true;
response->message = "机器人状态: " + status;
RCLCPP_INFO