3. gRPC 通信模式:一元RPC、服务端流式RPC、客户端流式RPC、双向流式RPC

gRPC 基于 HTTP/2 协议,提供了四种通信模式(Communication Pattern),覆盖了从简单的请求-应答到复杂的流式处理场景。理解这四种模式是设计高性能微服务通信架构的基础,也是后续实施限流、熔断与数据传输优化的前提。

3.1 四种通信模式概述

gRPC 的通信模式由 Protobuf 服务定义中的 rpc 关键字和 stream 修饰符决定。下表从调用方式、数据流向和典型场景三个维度进行对比:

模式名称 定义语法 客户端发送 服务端响应 典型场景
一元 RPC rpc Foo(Request) returns (Response) 1 条消息 1 条消息 传统 REST 替代、查询、写入
服务端流式 RPC rpc Foo(Request) returns (stream Response) 1 条消息 多条消息(流) 日志推送、数据订阅、大文件下载
客户端流式 RPC rpc Foo(stream Request) returns (Response) 多条消息(流) 1 条消息 批量上传、传感器数据聚合
双向流式 RPC rpc Foo(stream Request) returns (stream Response) 多条消息(流) 多条消息(流) 实时聊天、协同编辑、游戏同步
关键认知: 流式 RPC 并非一次性发送所有数据,而是通过 HTTP/2 的帧(Frame)机制持续传输。服务端和客户端各自维护独立的读写循环,这是实现背压(Backpressure)和限流的基础。

3.2 一元 RPC(Unary RPC)

一元 RPC 是最基础的通信模式,与传统的 HTTP REST 调用在语义上最为接近。客户端发送一个请求,服务端返回一个响应,完成后连接关闭。

  • 优点: 实现简单,调试方便,适合大多数 CRUD 操作。
  • 缺点: 无法处理大体积数据的分批传输,客户端必须等待完整响应。
  • 性能关注点: 序列化/反序列化开销、连接池复用、超时配置。
// Protobuf 定义
service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

// 客户端调用(Java 示例)
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
GetUserResponse response = stub.getUser(GetUserRequest.newBuilder().setUserId(1001).build());
System.out.println("User name: " + response.getName());

3.3 服务端流式 RPC(Server Streaming RPC)

客户端发送一个请求,服务端以流的形式返回多条消息。客户端通过迭代器或回调逐条读取,直到服务端发送 onCompleted 信号。

  • 适用场景: 实时日志流、股票行情推送、大数据集的分页查询。
  • 流量控制: 客户端可以通过 FlowControl 机制控制接收速率,防止内存溢出。
  • 限流策略: 服务端可在流中插入速率限制信号,或直接拒绝新流请求。
// Protobuf 定义
service LogService {
  rpc SubscribeLogs (LogRequest) returns (stream LogEntry);
}

// 服务端实现(Go 示例)
func (s *logServer) SubscribeLogs(req *pb.LogRequest, stream pb.LogService_SubscribeLogsServer) error {
    for _, entry := range fetchLogs(req.GetLevel()) {
        if err := stream.Send(entry); err != nil {
            return err
        }
        time.Sleep(100 * time.Millisecond) // 模拟限速
    }
    return nil
}

3.4 客户端流式 RPC(Client Streaming RPC)

客户端发送多条消息流,服务端在接收完所有消息后返回一个聚合响应。客户端通过 onNext 发送数据,最后调用 onCompleted 通知服务端结束。

  • 适用场景: 批量数据上传、IoT 传感器数据上报、文件分片上传。
  • 数据完整性: 服务端通常会在收到 onCompleted 后校验数据完整性,再返回结果。
  • 限流熔断: 服务端可以设置最大消息数或最大字节数阈值,超出后主动取消流。
// Protobuf 定义
service UploadService {
  rpc UploadFile (stream FileChunk) returns (UploadStatus);
}

// 客户端发送(Java 示例)
StreamObserver<UploadStatus> responseObserver = new StreamObserver<>() {
    public void onNext(UploadStatus status) {
        System.out.println("Upload result: " + status.getSuccess());
    }
    public void onError(Throwable t) { /* 处理错误 */ }
    public void onCompleted() { /* 完成 */ }
};
StreamObserver<FileChunk> requestObserver = stub.uploadFile(responseObserver);
for (FileChunk chunk : splitFile("data.bin")) {
    requestObserver.onNext(chunk);
}
requestObserver.onCompleted();

3.5 双向流式 RPC(Bidirectional Streaming RPC)

客户端和服务端可以独立、并发地发送和接收消息流。双方都不需要等待对方完成,实现了真正的全双工通信。

  • 适用场景: 实时聊天、多人协作编辑、游戏状态同步、实时音视频信令。
  • 架构挑战: 连接管理、消息顺序保证、心跳保活、背压处理。
  • 限流熔断: 双向流需要同时监控读写速率,任何一方过载都应触发熔断。
// Protobuf 定义
service ChatService {
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

// 服务端双向流处理(Go 示例)
func (s *chatServer) Chat(stream pb.ChatService_ChatServer) error {
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            return nil
        }
        if err != nil {
            return err
        }
        // 处理消息并广播
        response := &pb.ChatMessage{Content: "Echo: " + msg.GetContent()}
        if err := stream.Send(response); err != nil {
            return err
        }
    }
}
架构建议: 在微服务架构中,一元 RPC 应作为默认通信方式;服务端流式 RPC 适合数据推送;双向流式 RPC 应谨慎使用,仅在需要低延迟双向交互时引入,并配合连接池和心跳机制。

3.6 四种模式的数据流对比图

下图从数据流向和时序角度,直观对比四种通信模式的核心差异:

gRPC 四种通信模式数据流向对比 一元 RPC 客户端 服务端 请求 响应 服务端流式 RPC 客户端 服务端 请求 流式响应 客户端流式 RPC 客户端 服务端 流式请求 响应 双向流式 RPC 客户端 服务端 双向独立流 说明:箭头方向表示数据流向;流式模式中,多条消息通过同一个 HTTP/2 流持续传输,无需重复建立连接。

3.7 模式选择决策树

在实际架构设计中,可以按照以下决策路径快速选择合适的通信模式:

  1. 是否需要服务端主动推送?
    • 是 → 考虑 服务端流式 RPC 或 双向流式 RPC
    • 否 → 进入下一步
  2. 客户端是否需要发送大量数据?
    • 是 → 考虑 客户端流式 RPC
    • 否 → 进入下一步
  3. 是否需要实时双向交互?
    • 是 → 选择 双向流式 RPC
    • 否 → 选择 一元 RPC
避坑提醒: 不要为了“未来扩展”而盲目使用双向流式 RPC。双向流会显著增加连接管理的复杂度,且对网络稳定性要求更高。在不需要双向推送的场景下,一元 RPC 配合轮询或 Webhook 往往是更稳健的选择。

3.8 四种模式在限流熔断中的差异化处理

不同的通信模式对限流和熔断策略有直接影响。下表总结了各模式下的关键考量:

通信模式 限流粒度 熔断触发条件 推荐策略
一元 RPC 请求数/秒(QPS) 错误率 > 50% 或延迟 > 阈值 令牌桶 + 熔断器
服务端流式 RPC 流数量 + 消息发送速率 流建立失败率 > 30% 连接池限制 + 速率限制
客户端流式 RPC 消息接收速率 + 总数据量 内存占用 > 80% 或超时 背压 + 窗口大小控制
双向流式 RPC 读写双向速率 + 活跃流数 任一方速率异常或连接中断 双向速率限制 + 心跳检测