4. 一元 RPC(Unary RPC)的定义、客户端与服务端实现流程、同步与异步调用
4.1 一元 RPC 的定义与通信模型
一元 RPC(Unary RPC)是 gRPC 中最基础、最常用的通信模式。其核心特征是:客户端发送一个请求消息,服务端返回一个响应消息。这种模式在语义上等价于传统的 HTTP 请求-响应模型,但底层基于 HTTP/2 协议,具备二进制编码、多路复用、头部压缩等性能优势。
核心特征: 1 请求 → 1 响应,连接复用,双向元数据传递。
一元 RPC 的通信流程可抽象为以下步骤:
- 客户端发起调用:客户端 Stub 将请求参数序列化为 Protobuf 二进制消息,通过 HTTP/2 Stream 发送。
- 服务端接收并处理:服务端框架反序列化请求,路由到对应的业务方法执行。
- 服务端返回响应:业务方法返回结果,框架序列化后通过同一 Stream 发送回客户端。
- 客户端接收结果:客户端 Stub 反序列化响应,完成一次 RPC 调用。
4.2 客户端与服务端实现流程
4.2.1 定义 Protobuf 服务
所有 gRPC 通信的起点是 .proto 文件。在一元 RPC 中,使用 rpc 关键字声明方法,指定一个输入消息类型和一个输出消息类型。
syntax = "proto3";
package ecommerce;
service OrderService {
// 一元 RPC:根据订单 ID 查询订单详情
rpc GetOrder(GetOrderRequest) returns (GetOrderResponse);
}
message GetOrderRequest {
string order_id = 1;
}
message GetOrderResponse {
string order_id = 1;
string product_name = 2;
int32 quantity = 3;
double price = 4;
string status = 5;
}
提示: 一元 RPC 的
.proto 定义中,returns 关键字后只能跟一个消息类型。如果需要返回多个字段,应封装在复合消息中。
4.2.2 服务端实现流程
服务端需要实现 .proto 中定义的接口。以下为 Java 语言的服务端实现示例:
// 服务端业务逻辑实现类
public class OrderServiceImpl extends OrderServiceGrpc.OrderServiceImplBase {
@Override
public void getOrder(GetOrderRequest request,
StreamObserver<GetOrderResponse> responseObserver) {
// 1. 解析请求参数
String orderId = request.getOrderId();
System.out.println("Received request for order: " + orderId);
// 2. 执行业务逻辑(如查询数据库)
GetOrderResponse response = GetOrderResponse.newBuilder()
.setOrderId(orderId)
.setProductName("Wireless Mouse")
.setQuantity(2)
.setPrice(49.99)
.setStatus("SHIPPED")
.build();
// 3. 发送响应并完成调用
responseObserver.onNext(response);
responseObserver.onCompleted();
}
}
// 启动 gRPC 服务端
public class OrderServer {
public static void main(String[] args) throws Exception {
Server server = ServerBuilder.forPort(50051)
.addService(new OrderServiceImpl())
.build()
.start();
System.out.println("Server started on port 50051");
server.awaitTermination();
}
}
4.2.3 客户端实现流程
客户端通过 Stub 发起调用。gRPC 为每个服务生成两种 Stub:阻塞式(同步)Stub 和 非阻塞式(异步)Stub。
// 客户端代码
public class OrderClient {
public static void main(String[] args) {
// 1. 创建 Channel
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
// 2. 创建同步 Stub
OrderServiceGrpc.OrderServiceBlockingStub blockingStub =
OrderServiceGrpc.newBlockingStub(channel);
// 3. 构造请求
GetOrderRequest request = GetOrderRequest.newBuilder()
.setOrderId("ORD-2024-001")
.build();
// 4. 发起同步调用(阻塞等待响应)
GetOrderResponse response = blockingStub.getOrder(request);
// 5. 处理响应
System.out.println("Order Status: " + response.getStatus());
System.out.println("Product: " + response.getProductName());
// 6. 关闭连接
channel.shutdown();
}
}
4.3 同步调用与异步调用
gRPC 为一元 RPC 提供了两种调用方式,分别对应不同的应用场景。下表对比了二者的核心差异:
| 对比维度 | 同步调用(Blocking Stub) | 异步调用(Future Stub / ListenableFuture Stub) |
|---|---|---|
| 线程模型 | 调用线程阻塞,等待响应返回 | 调用线程立即返回,通过回调或 Future 获取结果 |
| 编程复杂度 | 低,代码顺序执行,易于理解 | 中高,需要处理回调或 Future 链 |
| 吞吐量 | 较低,线程在等待期间无法处理其他请求 | 较高,线程可复用处理其他任务 |
| 适用场景 | 简单查询、脚本工具、低并发环境 | 高并发服务、微服务间调用、事件驱动架构 |
| 错误处理 | 直接抛出异常,try-catch 捕获 | 通过回调或 Future.get() 捕获 ExecutionException |
4.3.1 同步调用详解
同步调用使用 BlockingStub,调用会阻塞当前线程直到服务端返回响应或超时。其内部实现基于 HTTP/2 的流式传输,但对开发者屏蔽了底层细节。
注意: 在同步调用中,如果服务端处理时间较长(如超过 5 秒),应设置超时时间,避免客户端线程长时间挂起。使用
blockingStub.withDeadlineAfter(5, TimeUnit.SECONDS).getOrder(request);。
4.3.2 异步调用详解
异步调用使用 FutureStub 或 ListenableFutureStub,调用立即返回一个 ListenableFuture 对象,开发者可以注册回调或通过 get() 方法在需要时获取结果。
// 异步客户端示例
public class AsyncOrderClient {
public static void main(String[] args) throws Exception {
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
// 创建异步 Stub
OrderServiceGrpc.OrderServiceFutureStub futureStub =
OrderServiceGrpc.newFutureStub(channel);
GetOrderRequest request = GetOrderRequest.newBuilder()
.setOrderId("ORD-2024-002")
.build();
// 发起异步调用,立即返回 ListenableFuture
ListenableFuture<GetOrderResponse> future = futureStub.getOrder(request);
// 方式一:注册回调(推荐)
Futures.addCallback(future, new FutureCallback<GetOrderResponse>() {
@Override
public void onSuccess(GetOrderResponse response) {
System.out.println("Async response: " + response.getStatus());
}
@Override
public void onFailure(Throwable t) {
System.err.println("Async call failed: " + t.getMessage());
}
}, MoreExecutors.directExecutor());
// 方式二:阻塞等待(不推荐,会失去异步优势)
// GetOrderResponse response = future.get(5, TimeUnit.SECONDS);
// 保持进程运行,等待回调执行
Thread.sleep(2000);
channel.shutdown();
}
}
4.4 一元 RPC 的最佳实践与避坑指南
关键实践要点:
- 消息大小限制: gRPC 默认消息大小为 4MB,超过时需在服务端和客户端同时配置
maxInboundMessageSize。 - 超时控制: 始终为客户端调用设置超时,避免因服务端异常导致客户端线程永久阻塞。
- 错误处理: 服务端通过
responseObserver.onError()返回 gRPC 状态码,客户端应捕获StatusRuntimeException并解析状态码。 - 连接复用: 客户端应复用
Channel实例,避免为每次调用创建新连接。
4.4.1 常见错误场景与处理
| 错误场景 | gRPC 状态码 | 处理建议 |
|---|---|---|
| 请求超时 | DEADLINE_EXCEEDED |
增加超时时间或优化服务端处理性能 |
| 服务端未启动 | UNAVAILABLE |
实现客户端重试机制,使用健康检查 |
| 请求消息过大 | RESOURCE_EXHAUSTED |
增大 maxInboundMessageSize 或拆分请求 |
| 参数校验失败 | INVALID_ARGUMENT |
在服务端业务逻辑中主动校验并返回错误 |
4.4.2 选择同步还是异步?
决策树可以帮助开发者快速选择:
- 如果调用方是用户请求线程(如 Web 控制器),且并发量不高 → 使用同步调用,代码更简洁。
- 如果调用方是后台批处理任务,需要同时发起多个独立 RPC → 使用异步调用,利用
Futures.allAsList()聚合结果。 - 如果系统对吞吐量有严格要求(如网关服务)→ 优先使用异步调用,配合线程池优化。
- 如果团队对响应式编程不熟悉 → 从同步调用开始,后续逐步迁移到异步。
经验之谈: 在微服务架构中,内部服务间调用推荐使用异步模式;对外提供 REST 网关时,网关层通常使用同步模式将 gRPC 响应转换为 HTTP 响应,以简化调用链。