4. 一元 RPC(Unary RPC)的定义、客户端与服务端实现流程、同步与异步调用

4.1 一元 RPC 的定义与通信模型

一元 RPC(Unary RPC)是 gRPC 中最基础、最常用的通信模式。其核心特征是:客户端发送一个请求消息,服务端返回一个响应消息。这种模式在语义上等价于传统的 HTTP 请求-响应模型,但底层基于 HTTP/2 协议,具备二进制编码、多路复用、头部压缩等性能优势。

核心特征: 1 请求 → 1 响应,连接复用,双向元数据传递。

一元 RPC 的通信流程可抽象为以下步骤:

  1. 客户端发起调用:客户端 Stub 将请求参数序列化为 Protobuf 二进制消息,通过 HTTP/2 Stream 发送。
  2. 服务端接收并处理:服务端框架反序列化请求,路由到对应的业务方法执行。
  3. 服务端返回响应:业务方法返回结果,框架序列化后通过同一 Stream 发送回客户端。
  4. 客户端接收结果:客户端 Stub 反序列化响应,完成一次 RPC 调用。
一元 RPC 通信流程 客户端 Client Stub 服务端 Server Stub ① 请求 (Request) Protobuf 序列化 · HTTP/2 Stream ② 响应 (Response) Protobuf 反序列化 · 状态码 一元 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();
    }
}
同步调用 vs 异步调用时序对比 同步调用 (BlockingStub) 客户端线程 服务端 请求 线程阻塞 等待响应 业务处理 ~50ms 响应 继续执行 异步调用 (FutureStub) 客户端线程 服务端 请求 立即返回 业务处理 ~50ms 继续其他工作 回调响应 回调处理

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 响应,以简化调用链。