2. gRPC基础回顾:Protocol Buffers定义、gRPC服务端与客户端快速搭建
在深入契约压测之前,必须牢固掌握gRPC的核心基础。gRPC是一个高性能、开源、通用的RPC框架,基于HTTP/2协议,默认使用Protocol Buffers(protobuf)作为接口定义语言(IDL)和序列化工具。本章将快速回顾protobuf的定义语法,并搭建一个完整的gRPC服务端与客户端。
2.1 Protocol Buffers 定义
Protocol Buffers是一种语言无关、平台无关、可扩展的序列化结构数据格式。在gRPC中,我们使用.proto文件定义服务接口和消息结构。
2.1.1 消息类型定义
以下是一个典型的用户服务消息定义,包含基本字段、嵌套类型和枚举:
syntax = "proto3";
package userpb;
option go_package = "github.com/example/userpb";
// 用户状态枚举
enum UserStatus {
USER_STATUS_UNSPECIFIED = 0;
USER_STATUS_ACTIVE = 1;
USER_STATUS_INACTIVE = 2;
USER_STATUS_BANNED = 3;
}
// 用户地址嵌套消息
message Address {
string street = 1;
string city = 2;
string zip_code = 3;
}
// 用户主消息
message User {
string user_id = 1;
string name = 2;
string email = 3;
UserStatus status = 4;
Address address = 5;
repeated string tags = 6;
map<string, string> metadata = 7;
}
// 请求与响应消息
message GetUserRequest {
string user_id = 1;
}
message GetUserResponse {
User user = 1;
}
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2;
}
2.1.2 服务接口定义
在同一个.proto文件中,使用service关键字定义RPC方法:
service UserService {
// 一元RPC:获取单个用户
rpc GetUser(GetUserRequest) returns (GetUserResponse);
// 服务端流式RPC:批量获取用户列表
rpc ListUsers(ListUsersRequest) returns (stream ListUsersResponse);
// 客户端流式RPC:批量创建用户
rpc CreateUsers(stream User) returns (ListUsersResponse);
// 双向流式RPC:实时聊天或数据交换
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message ChatMessage {
string user_id = 1;
string content = 2;
int64 timestamp = 3;
}
2.1.3 字段编号与兼容性规则
| 规则 | 说明 | 示例 |
|---|---|---|
| 字段编号范围 | 1~15占用1字节编码,16~2047占用2字节 | 频繁字段使用1~15 |
| 不可修改编号 | 一旦发布,字段编号不可更改 | 删除字段需保留编号并标记reserved |
| 向前兼容 | 新字段使用新编号,旧代码忽略未知字段 | 添加字段不影响已有服务 |
| 向后兼容 | 旧字段可被新代码读取,只要类型兼容 | int32→int64安全 |
| reserved关键字 | 防止字段编号或名称被复用 | reserved 2, 3, 10 to 15; |
最佳实践:在压测场景中,消息定义应尽量精简,避免嵌套过深或大量repeated字段,否则会显著增加序列化/反序列化开销,影响压测吞吐量。
2.2 gRPC服务端快速搭建
使用Go语言搭建gRPC服务端,需要先安装protoc编译器及Go插件,然后生成Go代码,最后实现服务接口。
2.2.1 环境准备与代码生成
# 安装protoc编译器(macOS示例)
brew install protobuf
# 安装Go插件
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 生成Go代码
protoc --go_out=. --go-grpc_out=. \
--go_opt=paths=source_relative \
--go-grpc_opt=paths=source_relative \
userpb/user.proto
生成的文件结构:
userpb/
├── user.pb.go # 消息类型的Go结构体
└── user_grpc.pb.go # 服务端/客户端接口与注册代码
2.2.2 实现服务端
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
"google.golang.org/grpc/reflection"
pb "github.com/example/userpb"
)
// 实现UserServiceServer接口
type userServer struct {
pb.UnimplementedUserServiceServer
}
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
// 模拟数据库查询
user := &pb.User{
UserId: req.UserId,
Name: "Alice",
Email: "alice@example.com",
Status: pb.UserStatus_USER_STATUS_ACTIVE,
}
return &pb.GetUserResponse{User: user}, nil
}
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
// 模拟分页返回多个用户
users := []*pb.User{
{UserId: "1", Name: "Alice"},
{UserId: "2", Name: "Bob"},
}
for _, u := range users {
if err := stream.Send(&pb.ListUsersResponse{Users: []*pb.User{u}}); err != nil {
return err
}
}
return nil
}
func main() {
lis, err := net.Listen("tcp", ":50051")
if err != nil {
log.Fatalf("failed to listen: %v", err)
}
s := grpc.NewServer()
pb.RegisterUserServiceServer(s, &userServer{})
// 开启反射,方便grpcurl调试
reflection.Register(s)
log.Printf("gRPC server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
2.3 gRPC客户端快速搭建
客户端通过服务端地址建立连接,调用生成的桩代码发起RPC请求。
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/example/userpb"
)
func main() {
// 建立连接(压测时建议使用连接池)
conn, err := grpc.Dial(
"localhost:50051",
grpc.WithTransportCredentials(insecure.NewCredentials()),
grpc.WithBlock(),
)
if err != nil {
log.Fatalf("did not connect: %v", err)
}
defer conn.Close()
client := pb.NewUserServiceClient(conn)
// 一元RPC调用
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
resp, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: "123"})
if err != nil {
log.Fatalf("GetUser failed: %v", err)
}
log.Printf("User: %+v", resp.User)
// 服务端流式RPC调用
streamCtx, streamCancel := context.WithTimeout(context.Background(), 5*time.Second)
defer streamCancel()
stream, err := client.ListUsers(streamCtx, &pb.ListUsersRequest{PageSize: 10})
if err != nil {
log.Fatalf("ListUsers failed: %v", err)
}
for {
item, err := stream.Recv()
if err != nil {
break
}
log.Printf("Received user: %+v", item.Users)
}
}
压测注意:客户端在压测场景中应复用
grpc.ClientConn连接,避免每次请求都创建新连接。gRPC连接默认使用HTTP/2多路复用,一个连接可承载数千并发流,但需注意连接数过多会导致服务端内存压力。
2.4 gRPC通信流程与协议栈
下图展示了gRPC从客户端到服务端的完整通信流程,以及各层协议栈的对应关系:
2.5 gRPC四种通信模式对比
| 模式 | 客户端请求 | 服务端响应 | 典型场景 | 压测关注点 |
|---|---|---|---|---|
| 一元RPC | 1个请求 | 1个响应 | 查询用户、下单 | 请求延迟、吞吐量 |
| 服务端流式 | 1个请求 | 多个响应 | 批量拉取、日志推送 | 流控、背压、内存 |
| 客户端流式 | 多个请求 | 1个响应 | 批量上传、聚合 | 发送速率、缓冲 |
| 双向流式 | 多个请求 | 多个响应 | 实时聊天、数据同步 | 全双工并发、顺序 |
2.6 本章小结
通过本章回顾,我们掌握了:
- Protocol Buffers定义:消息类型、枚举、嵌套、map、repeated字段,以及服务接口的四种RPC模式。
- 服务端搭建:从protoc代码生成到实现
RegisterUserServiceServer接口,并启动gRPC服务。 - 客户端搭建:建立连接、创建Stub、发起一元和流式RPC调用。
- 通信流程:从应用层到HTTP/2帧层、TCP传输层的完整协议栈。