你问"今晚有特价套餐吗?"(请求),客服查了一下回答"有,芝士汉堡套餐18元"(响应)。打电话过程中,你等回复,打完电话挂断,这事就完了。服务通信就是这样,一锤子买卖。
服务通信是ROS2中一对一的同步通信方式:一个节点作为服务端(Server)提供功能,另一个节点作为客户端(Client)发起请求,收到响应后完成交互。底层依赖DDS的请求-响应机制,但接口对用户屏蔽了DDS细节。
表:服务器与客户端消息结构对比
| 消息类型 | 方向 | 包含内容 | 是否必须 |
|---|---|---|---|
| 请求(Request) | 客户端→服务端 | 输入参数(如目标角度) | 必须 |
| 响应(Response) | 服务端→客户端 | 输出结果(如状态码、实际位置) | 必须 |
服务接口用
# example_interfaces/srv/AddTwoInts.srv
int64 a
int64 b
---
int64 sum
实际的.srv文件放在服务的功能包中的srv/目录下,并在CMakeLists.txt中注册。常用内置服务在example_interfaces包和std_srvs包中。
服务端节点负责接收请求,处理逻辑,返回响应。核心是定义一个回调函数,每次收到请求时调用。
class ServerNode : public rclcpp::Node
{
public:
ServerNode() : Node("server_node")
{
service_ = create_service<example_interfaces::srv::AddTwoInts>(
"add_two_ints",
std::bind(&ServerNode::handle_request, this, std::placeholders::_1, std::placeholders::_2));
}
private:
void handle_request(
const std::shared_ptr<example_interfaces::srv::AddTwoInts::Request> req,
std::shared_ptr<example_interfaces::srv::AddTwoInts::Response> res)
{
res->sum = req->a + req->b;
RCLCPP_INFO(get_logger(), "收到请求: %ld + %ld = %ld", req->a, req->b, res->sum);
}
rclcpp::Service<example_interfaces::srv::AddTwoInts>::SharedPtr service_;
};
回调函数中的两个参数分别是请求和响应。请求是常量指针,不能修改;响应是共享指针,你负责填充。服务端不需要任何额外的spin等待,rclcpp默认会在后台处理服务请求。
编者提示: 实际开发中,一个常见问题是服务端执行长时间任务(如电机转到90度需要2秒),此时其他服务请求会被阻塞。可以用多线程触发回调,设置rclcpp::NodeOptions中的use_intra_process_buffer为false,或者将耗时操作放到另一个线程中。
客户端节点发起请求,然后等待响应。响应可以同步等待(阻塞当前线程)或异步等待(不阻塞)。
class ClientNode : public rclcpp::Node
{
public:
ClientNode() : Node("client_node")
{
client_ = create_client<example_interfaces::srv::AddTwoInts>("add_two_ints");
}
void send_request()
{
while (!client_->wait_for_service(std::chrono::seconds(1))) {
RCLCPP_WARN(get_logger(), "等待服务端就绪...");
}
auto request = std::make_shared<example_interfaces::srv::AddTwoInts::Request>();
request->a = 3;
request->b = 5;
auto result = client_->async_send_request(request);
// 处理响应...建议用回调或spin_until_future_complete
}
private:
rclcpp::Client<example_interfaces::srv::AddTwoInts>::SharedPtr client_;
};
如果客户端先启动但服务端还没起来,wait_for_service会循环等待,避免段错误。这是实际开发中必须处理的情况。
表:服务通信与话题通信关键区别
| 特征 | 话题通信 | 服务通信 |
|---|---|---|
| 模式 | 发布/订阅 | 请求/响应 |
| 是否等待 | 不等待,消息可能丢失 | 等待响应(超时可设) |
| 并发连接 | 一对多、多对多 | 一对一(一客户端一服务端) |
| 适用场景 | 传感器数据、系统状态 | 命令控制、调用服务 |
同一个服务端可以同时被多个客户端调用。默认情况下,ROS2服务端是线程安全的,多个请求会依次处理。如果你需要并发处理,可以使用rclcpp::NodeOptions设置CallbackGroup。
实际项目里,我见过一个机器人节点提供"舵机角度设定"服务,多个控制节点同时发送请求。服务端一个接一个处理,如果处理时间短(<1ms),感觉是并发的;如果处理时间长,客户端会超时,要在客户端设置超时参数。
如果你看到"Failed to call service"或"Service not available"报错,大概率是服务端节点还没启动,或服务名拼写错误。先在终端用 ros2 service list 查看所有可用服务。
任务: 编写一个简单的服务端和客户端(在同一个功能包中)。服务端接收两个整数,返回它们的乘积。客户端向服务端发送(4, 6),并打印结果"4 * 6 = 24"。