4. Go gRPC 客户端实现:基于 Protobuf 桩代码的 RPC 调用
在完成 .proto 文件定义并通过 protoc 生成 Go 桩代码后,客户端开发者无需关心网络传输细节,只需调用生成的桩代码即可发起 RPC 调用。本章聚焦于两种最常用的通信模式:Unary RPC(一元调用)与 Client Streaming RPC(客户端流式调用),并给出完整的 Go 实现示例。
4.1 客户端桩代码结构概览
假设我们定义了一个 order.proto 文件,包含以下两个 RPC 方法:
service OrderService {
rpc CreateOrder (CreateOrderRequest) returns (CreateOrderResponse);
rpc BatchSubmitOrders (stream Order) returns (BatchSummary);
}
执行 protoc 后,生成的 Go 桩代码主要包含以下核心类型:
| 桩代码元素 | 说明 |
|---|---|
OrderServiceClient |
客户端接口,定义所有 RPC 方法签名 |
orderServiceClient |
接口的具体实现,封装 gRPC 连接与调用逻辑 |
NewOrderServiceClient(cc grpc.ClientConnInterface) |
构造函数,传入连接对象返回客户端实例 |
CreateOrder(ctx, req, opts...) |
Unary RPC 调用方法,返回响应与错误 |
BatchSubmitOrders(ctx, opts...) |
Client Streaming RPC 调用方法,返回流对象 |
OrderService_BatchSubmitOrdersClient |
客户端流接口,提供 Send、CloseAndRecv 方法 |
提示: 桩代码中的
opts ...grpc.CallOption 参数可用于传递超时、元数据、压缩等调用选项,在压测场景中常用来设置 grpc.WaitForReady(true) 或自定义拦截器。
4.2 Unary RPC 调用实现
Unary RPC 是最简单的请求-响应模式,客户端发送一个请求,服务端返回一个响应。实现步骤如下:
- 建立连接:使用
grpc.Dial创建到服务端的连接。 - 创建客户端:调用
pb.NewOrderServiceClient传入连接。 - 构造请求:填充
CreateOrderRequest消息字段。 - 发起调用:调用
client.CreateOrder并处理响应。
package main
import (
"context"
"log"
"time"
pb "path/to/proto/gen/go/order/v1"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
)
func main() {
// 1. 建立连接(压测时建议使用 grpc.WithBlock 确保连接就绪)
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
grpc.WithTimeout(5*time.Second),
)
if err != nil {
log.Fatalf("failed to dial: %v", err)
}
defer conn.Close()
// 2. 创建客户端
client := pb.NewOrderServiceClient(conn)
// 3. 构造请求
req := &pb.CreateOrderRequest{
UserId: "user_1001",
Sku: "SKU-2024-001",
Quantity: 2,
}
// 4. 发起 Unary 调用(设置 3 秒超时上下文)
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
resp, err := client.CreateOrder(ctx, req)
if err != nil {
log.Fatalf("CreateOrder failed: %v", err)
}
log.Printf("order created: id=%s, status=%s", resp.OrderId, resp.Status)
}
压测要点: 在性能测试中,
grpc.Dial 应复用连接,避免每次请求都新建连接。同时建议使用 grpc.WithDefaultCallOptions(grpc.WaitForReady(true)) 让客户端在连接暂时不可用时等待,而非立即失败。
4.3 Client Streaming RPC 调用实现
Client Streaming RPC 允许客户端发送多条消息,最后关闭流并等待服务端返回汇总响应。典型场景包括批量提交订单、上传日志等。实现步骤:
- 获取流对象:调用
client.BatchSubmitOrders返回OrderService_BatchSubmitOrdersClient。 - 发送消息:循环调用
stream.Send(msg)发送每条数据。 - 关闭并接收:调用
stream.CloseAndRecv()结束发送并获取服务端响应。
func batchSubmitOrders(client pb.OrderServiceClient, orders []*pb.Order) (*pb.BatchSummary, error) {
// 1. 获取流对象
stream, err := client.BatchSubmitOrders(context.Background())
if err != nil {
return nil, err
}
// 2. 循环发送每条订单
for _, order := range orders {
if err := stream.Send(order); err != nil {
// 发送失败时,可尝试 Recv 获取服务端错误
return nil, err
}
}
// 3. 关闭发送端并接收汇总响应
summary, err := stream.CloseAndRecv()
if err != nil {
return nil, err
}
return summary, nil
}
// 调用示例
func main() {
conn, _ := grpc.Dial("localhost:50051", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
client := pb.NewOrderServiceClient(conn)
orders := []*pb.Order{
{UserId: "u1", Sku: "A001", Quantity: 1},
{UserId: "u2", Sku: "B002", Quantity: 3},
{UserId: "u3", Sku: "C003", Quantity: 5},
}
summary, err := batchSubmitOrders(client, orders)
if err != nil {
log.Fatalf("batch submit failed: %v", err)
}
log.Printf("batch summary: total=%d, success=%d, failed=%d",
summary.TotalCount, summary.SuccessCount, summary.FailedCount)
}
注意: Client Streaming 中,
stream.Send 是阻塞的,受 gRPC 流量控制和网络延迟影响。压测时需关注 Send 的耗时分布,避免因发送过慢导致整体吞吐下降。建议在发送循环中增加批量大小控制或并发发送。
4.4 两种 RPC 模式对比
| 对比维度 | Unary RPC | Client Streaming RPC |
|---|---|---|
| 消息方向 | 单请求 → 单响应 | 多请求 → 单响应 |
| 适用场景 | 查询、创建单条资源 | 批量提交、日志上传、数据导入 |
| 客户端 API | client.Method(ctx, req) |
stream.Send() + stream.CloseAndRecv() |
| 错误处理 | 调用返回时直接获取 error | Send 阶段错误需主动检查;CloseAndRecv 返回最终错误 |
| 压测关注点 | QPS、延迟、连接复用 | 发送速率、流控、内存占用 |
4.5 客户端调用流程总览
下图展示了从建立连接到完成两种 RPC 调用的完整流程:
4.6 客户端调用最佳实践
- 连接复用:一个
grpc.ClientConn可被多个 goroutine 并发使用,压测时只需建立少量连接即可支撑高并发。 - 超时控制:始终使用带超时的
context.Context,避免因服务端异常导致客户端 goroutine 泄漏。 - 错误处理:Unary 调用直接检查 error;Client Streaming 中
Send返回 error 后应调用stream.RecvMsg或CloseAndRecv获取服务端详细错误。 - 流控感知:Client Streaming 发送速率受 gRPC 流控窗口限制,压测时需监控
Send的阻塞时间,必要时调整InitialWindowSize参数。 - 拦截器注入:通过
grpc.WithUnaryInterceptor和grpc.WithStreamInterceptor可注入客户端拦截器,用于统一日志、指标采集和重试逻辑。
压测建议: 在编写压测脚本时,建议将 Unary 和 Client Streaming 分别封装为独立的
Runner 结构体,便于后续在测试框架中按场景组合调用。同时,使用 sync.Pool 复用请求消息对象,减少 GC 压力。