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 调用,大幅减少了连接开销。核心通信流程如下:
- 服务定义:使用 protobuf 在
.proto文件中定义服务接口和消息结构。 - 代码生成:通过 protoc 编译器生成客户端 Stub 和服务器骨架代码。
- 连接建立:客户端通过 HTTP/2 升级握手与服务器建立持久连接。
- 数据序列化:请求与响应数据被序列化为二进制 protobuf 格式,通过 HTTP/2 的 DATA 帧传输。
- 流控制:HTTP/2 的流控机制确保发送方不会压垮接收方。
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)
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() 的返回值,并设计带外信号(如专用的错误消息类型)来传递异常状态。事务边界在双向流中最为模糊,通常需要应用层协议(如序列号、确认机制)来保证一致性。
2.7 模式选择与错误处理策略总结
选择哪种 RPC 模式,直接决定了后续错误处理和事务边界设计的复杂度。以下表格总结了不同模式下的关键考量:
| 考量维度 | Unary | Server Streaming | Client Streaming | Bidirectional Streaming |
|---|---|---|---|---|
| 错误可见性 | 立即可见 | 流中任意点可见 | 仅在流关闭后可见 | 发送/接收独立可见 |
| 重试复杂度 | 低(幂等性易保证) | 中(需记录消费位置) | 高(需重新发送整个流) | 高(需同步双方状态) |
| 事务边界 | 单次调用 | 流开始到结束 | 流开始到结束 | 应用层定义 |
| 典型错误码 | NotFound, InvalidArgument, Internal | Unavailable, DataLoss, Canceled | DeadlineExceeded, ResourceExhausted | Unimplemented, Internal, Canceled |