2. gRPC 基础回顾:通信模型与四种 RPC 模式

在深入错误处理与事务边界之前,必须牢固掌握 gRPC 的通信模型及其四种核心 RPC 模式。gRPC 基于 HTTP/2 协议,利用 Protocol Buffers(protobuf)作为接口定义语言(IDL)和序列化工具,构建了高效、强类型的远程调用体系。本章将系统梳理其通信模型,并逐一剖析四种 RPC 模式的特点、适用场景与关键代码结构。

2.1 gRPC 通信模型总览

gRPC 采用客户端-服务器(C/S)架构,通信建立在 HTTP/2 的单一长连接之上。HTTP/2 的多路复用(Multiplexing)机制允许在同一个 TCP 连接上并发处理多个 RPC 调用,大幅减少了连接开销。核心通信流程如下:

  1. 服务定义:使用 protobuf 在 .proto 文件中定义服务接口和消息结构。
  2. 代码生成:通过 protoc 编译器生成客户端 Stub 和服务器骨架代码。
  3. 连接建立:客户端通过 HTTP/2 升级握手与服务器建立持久连接。
  4. 数据序列化:请求与响应数据被序列化为二进制 protobuf 格式,通过 HTTP/2 的 DATA 帧传输。
  5. 流控制:HTTP/2 的流控机制确保发送方不会压垮接收方。
关键认知: gRPC 的通信模型本质上是“协议层多路复用 + 应用层序列化”的组合。理解这一点,是后续分析错误传播和事务边界的基础。
gRPC 通信模型架构 gRPC 客户端 Client Stub 序列化 / 反序列化 HTTP/2 帧构造 连接管理 流控 gRPC 服务器 Server Stub 反序列化 / 序列化 HTTP/2 帧解析 服务实现 流控 HTTP/2 长连接 多路复用 (Multiplexing) 二进制分帧 头部压缩 (HPACK) 服务器推送 (Server Push)

2.2 四种 RPC 模式详解

gRPC 基于 HTTP/2 的流(Stream)概念,定义了四种 RPC 模式。这些模式决定了客户端与服务器之间消息交换的时序和数量,直接影响错误处理策略和事务边界的定义。

模式 客户端请求 服务器响应 典型场景 错误处理特点
Unary RPC 1 条消息 1 条消息 查询用户信息、创建订单 请求/响应均完整,错误在最终响应中返回
Server Streaming RPC 1 条消息 多条消息(流) 订阅事件、日志推送、大文件下载 流中任意消息出错,后续消息可能中断
Client Streaming RPC 多条消息(流) 1 条消息 批量上传、传感器数据聚合 客户端流中出错,服务器需等待流结束才能返回错误
Bidirectional Streaming RPC 多条消息(流) 多条消息(流) 实时聊天、游戏同步、双向数据管道 双方独立流,错误需通过带外信号或流关闭传递

2.3 模式一:Unary RPC(一元 RPC)

Unary RPC 是最简单的模式,客户端发送一个请求,服务器返回一个响应。其行为类似于传统的 HTTP REST 调用,但基于 HTTP/2 和 protobuf 提供了更低的延迟和更强的类型安全。

protobuf 定义示例:

service UserService {
  rpc GetUser (GetUserRequest) returns (GetUserResponse);
}

message GetUserRequest {
  int64 user_id = 1;
}

message GetUserResponse {
  string name = 1;
  string email = 2;
  int32 age = 3;
}

客户端调用(Go 语言示例):

conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
defer conn.Close()
client := pb.NewUserServiceClient(conn)

ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()

resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: 123})
if err != nil {
    // 处理 gRPC 错误(如 DeadlineExceeded, Unavailable 等)
    log.Fatalf("could not get user: %v", err)
}
log.Printf("User: %s, Email: %s", resp.Name, resp.Email)
错误处理要点: Unary 模式中,错误通常通过 err 返回值传递。gRPC 状态码(如 codes.NotFound、codes.Internal)和详细错误消息一起封装在 status.Error 中。客户端必须检查 err 并解析状态码,以决定重试、降级或报错。

2.4 模式二:Server Streaming RPC(服务器流式 RPC)

客户端发送一个请求,服务器返回一个消息流。客户端通过迭代读取流中的消息,直到流结束。此模式适用于服务器需要向客户端推送大量数据或持续事件的场景。

protobuf 定义示例:

service EventService {
  rpc SubscribeEvents (SubscribeRequest) returns (stream Event);
}

message SubscribeRequest {
  string topic = 1;
}

message Event {
  int64 id = 1;
  string payload = 2;
  int64 timestamp = 3;
}

客户端读取流(Go 语言示例):

stream, err := client.SubscribeEvents(ctx, &pb.SubscribeRequest{Topic: "orders"})
if err != nil {
    log.Fatalf("could not subscribe: %v", err)
}

for {
    event, err := stream.Recv()
    if err == io.EOF {
        // 流正常结束
        break
    }
    if err != nil {
        // 流中发生错误(如网络中断、服务器错误)
        log.Printf("error receiving event: %v", err)
        break
    }
    log.Printf("Received event: %d - %s", event.Id, event.Payload)
}
流中断处理: 服务器可以在流中的任意时刻返回一个错误(通过 stream.SendAndClose 或直接返回错误)。客户端必须对 stream.Recv() 返回的每个错误进行检查。常见的错误包括 codes.Canceled(客户端取消)、codes.Unavailable(服务器不可用)和 codes.DataLoss(流数据损坏)。

2.5 模式三:Client Streaming RPC(客户端流式 RPC)

客户端发送一个消息流,服务器在接收完所有消息后返回一个响应。此模式适用于客户端需要批量提交数据,然后等待服务器聚合处理的场景。

protobuf 定义示例:

service UploadService {
  rpc UploadChunks (stream Chunk) returns (UploadSummary);
}

message Chunk {
  bytes data = 1;
  int32 sequence_number = 2;
}

message UploadSummary {
  int64 total_bytes = 1;
  int32 chunk_count = 2;
}

客户端发送流(Go 语言示例):

stream, err := client.UploadChunks(ctx)
if err != nil {
    log.Fatalf("could not start upload: %v", err)
}

for _, chunk := range chunks {
    if err := stream.Send(chunk); err != nil {
        // 发送过程中出错(如流已关闭、网络问题)
        log.Printf("error sending chunk: %v", err)
        break
    }
}

summary, err := stream.CloseAndRecv()
if err != nil {
    // 服务器返回的错误(如校验失败、处理超时)
    log.Printf("upload failed: %v", err)
} else {
    log.Printf("Upload summary: %d bytes, %d chunks", summary.TotalBytes, summary.ChunkCount)
}
事务边界挑战: 在客户端流式模式中,服务器只有在客户端关闭流并调用 CloseAndRecv() 后,才能返回最终响应和错误。这意味着如果客户端在发送过程中遇到错误(如网络中断),服务器可能无法及时获知,导致事务状态不一致。实践中,客户端流通常需要配合心跳或超时机制来检测半开连接。

2.6 模式四:Bidirectional Streaming RPC(双向流式 RPC)

客户端和服务器可以独立地发送和接收消息流。双方的消息顺序在各自流内保持有序,但两个流之间没有严格的时序依赖。此模式适用于需要实时双向交互的场景。

protobuf 定义示例:

service ChatService {
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

message ChatMessage {
  string user = 1;
  string text = 2;
  int64 timestamp = 3;
}

客户端双向交互(Go 语言示例):

stream, err := client.Chat(ctx)
if err != nil {
    log.Fatalf("could not start chat: %v", err)
}

// 启动一个 goroutine 接收消息
go func() {
    for {
        msg, err := stream.Recv()
        if err == io.EOF {
            break
        }
        if err != nil {
            log.Printf("receive error: %v", err)
            break
        }
        log.Printf("[%s] %s", msg.User, msg.Text)
    }
}()

// 主 goroutine 发送消息
for _, msg := range messagesToSend {
    if err := stream.Send(msg); err != nil {
        log.Printf("send error: %v", err)
        break
    }
}
stream.CloseSend()
错误传播复杂性: 双向流中,发送和接收是独立的。服务器可以在接收客户端消息时返回错误(通过 stream.Send() 发送错误消息或关闭流),客户端也可能在发送过程中遇到错误。错误处理需要同时关注 Send() 和 Recv() 的返回值,并设计带外信号(如专用的错误消息类型)来传递异常状态。事务边界在双向流中最为模糊,通常需要应用层协议(如序列号、确认机制)来保证一致性。
四种 gRPC RPC 模式对比 Unary RPC 客户端 → 1 请求 服务器 → 1 响应 同步 / 异步 Server Streaming 客户端 → 1 请求 服务器 → N 响应 事件推送 / 日志 Client Streaming 客户端 → N 请求 服务器 → 1 响应 批量上传 / 聚合 Bidirectional 客户端 → N 请求 服务器 → N 响应 实时聊天 / 游戏 模式选择与错误处理策略 • Unary:错误处理最简单,适合请求-响应式事务,事务边界清晰。 • Server Streaming:流中断后需处理部分消费的数据,事务边界在流开始处。 • Client Streaming:服务器在流关闭前无法返回错误,事务边界在流结束时。 • Bidirectional Streaming:错误可能发生在任意方向,事务边界需应用层协议保障。 • 所有模式均支持 context 超时 和 取消传播,这是错误处理的第一道防线。

2.7 模式选择与错误处理策略总结

选择哪种 RPC 模式,直接决定了后续错误处理和事务边界设计的复杂度。以下表格总结了不同模式下的关键考量:

考量维度 Unary Server Streaming Client Streaming Bidirectional Streaming
错误可见性 立即可见 流中任意点可见 仅在流关闭后可见 发送/接收独立可见
重试复杂度 低(幂等性易保证) 中(需记录消费位置) 高(需重新发送整个流) 高(需同步双方状态)
事务边界 单次调用 流开始到结束 流开始到结束 应用层定义
典型错误码 NotFound, InvalidArgument, Internal Unavailable, DataLoss, Canceled DeadlineExceeded, ResourceExhausted Unimplemented, Internal, Canceled
工程建议: 在微服务架构中,优先使用 Unary 模式以降低错误处理和事务管理的复杂度。仅在确实需要流式交互(如大文件传输、实时事件推送)时才选择流式模式。对于流式模式,务必在服务设计阶段就明确错误传播路径和事务边界协议,避免在后期陷入“状态不一致”的泥潭。