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从客户端到服务端的完整通信流程,以及各层协议栈的对应关系:

gRPC通信流程与协议栈 客户端应用 Stub(桩代码) gRPC客户端层 HTTP/2 帧层 TCP 传输层 网络传输 Protobuf 二进制 HTTP/2 流多路复用 TLS 加密(可选) TCP 传输层 HTTP/2 帧层 gRPC服务端层 服务实现 业务逻辑 关键说明: 1. 客户端Stub将方法调用序列化为Protobuf二进制消息,通过HTTP/2流发送。 2. HTTP/2支持多路复用,一个TCP连接可承载多个并发RPC流,减少连接开销。 3. 服务端反序列化请求,执行业务逻辑,将响应序列化后返回。 4. 压测时需关注序列化/反序列化CPU消耗、HTTP/2流并发数、TCP连接数。

2.5 gRPC四种通信模式对比

模式 客户端请求 服务端响应 典型场景 压测关注点
一元RPC 1个请求 1个响应 查询用户、下单 请求延迟、吞吐量
服务端流式 1个请求 多个响应 批量拉取、日志推送 流控、背压、内存
客户端流式 多个请求 1个响应 批量上传、聚合 发送速率、缓冲
双向流式 多个请求 多个响应 实时聊天、数据同步 全双工并发、顺序

2.6 本章小结

通过本章回顾,我们掌握了:

  • Protocol Buffers定义:消息类型、枚举、嵌套、map、repeated字段,以及服务接口的四种RPC模式。
  • 服务端搭建:从protoc代码生成到实现RegisterUserServiceServer接口,并启动gRPC服务。
  • 客户端搭建:建立连接、创建Stub、发起一元和流式RPC调用。
  • 通信流程:从应用层到HTTP/2帧层、TCP传输层的完整协议栈。