2. gRPC 基础回顾:Protocol Buffers 语法、Go语言gRPC服务端与客户端快速搭建

在深入契约缓存一致性之前,必须夯实 gRPC 与 Protocol Buffers(protobuf)的基础。本章将快速回顾 protobuf 的核心语法,并手把手搭建一个 Go 语言的 gRPC 服务端与客户端,为后续的缓存实战铺平道路。

2.1 Protocol Buffers 语法速览

Protocol Buffers 是 gRPC 的接口定义语言(IDL)与序列化工具。一个 .proto 文件定义了服务接口和消息结构,是 gRPC 通信的“契约”。

2.1.1 消息结构(Message)

消息是 protobuf 中最基本的数据单元,使用 message 关键字定义。每个字段有类型、名称和唯一的编号。

syntax = "proto3"; // 指定使用 proto3 语法

package userpb;   // 包名,用于避免命名冲突

// 用户信息消息
message UserInfo {
  int32  id       = 1;  // 字段编号从1开始
  string name     = 2;
  string email    = 3;
  int32  age      = 4;
  repeated string tags = 5; // repeated 表示数组(切片)
}
字段编号规则: 1~15 的编号使用 1 字节编码,16~2047 使用 2 字节编码。频繁使用的字段应分配小编号以节省带宽。

2.1.2 服务定义(Service)

服务定义使用 service 关键字,内部声明 RPC 方法。每个方法指定输入消息和输出消息。

service UserService {
  // 一元 RPC:客户端发送一个请求,服务端返回一个响应
  rpc GetUser (GetUserRequest) returns (GetUserResponse);

  // 服务端流式 RPC:客户端发送一个请求,服务端返回一个流
  rpc ListUsers (ListUsersRequest) returns (stream UserInfo);

  // 客户端流式 RPC:客户端发送一个流,服务端返回一个响应
  rpc CreateUsers (stream CreateUserRequest) returns (CreateUsersResponse);

  // 双向流式 RPC:双方同时发送和接收流
  rpc Chat (stream ChatMessage) returns (stream ChatMessage);
}

2.1.3 常用标量类型对照表

.proto 类型 Go 类型 说明
double float64 64位浮点数
float float32 32位浮点数
int32 int32 可变长编码,负数效率低
int64 int64 可变长编码
uint32 uint32 无符号整数
bool bool 布尔值
string string UTF-8 字符串
bytes []byte 任意字节序列

2.2 从 .proto 生成 Go 代码

编写完 .proto 文件后,需要使用 protoc 编译器生成 Go 代码。需要安装 protoc 以及两个 Go 插件:protoc-gen-go 和 protoc-gen-go-grpc。

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

2.2.2 编译命令

protoc --go_out=. --go-grpc_out=. \
  --proto_path=proto \
  proto/user.proto

执行后,会在当前目录生成两个文件:

  • user.pb.go:包含消息的序列化/反序列化代码。
  • user_grpc.pb.go:包含服务端接口、客户端桩代码。

2.3 Go 语言 gRPC 服务端快速搭建

以下代码演示如何基于生成的桩代码,实现一个简单的 gRPC 服务端。

2.3.1 实现服务接口

package main

import (
    "context"
    "log"
    "net"

    "google.golang.org/grpc"
    pb "path/to/your/proto/package" // 替换为实际包路径
)

// server 结构体实现 UserServiceServer 接口
type server struct {
    pb.UnimplementedUserServiceServer
}

// GetUser 实现一元 RPC 方法
func (s *server) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.GetUserResponse, error) {
    // 模拟从数据库查询用户
    user := &pb.UserInfo{
        Id:    req.UserId,
        Name:  "Alice",
        Email: "alice@example.com",
        Age:   30,
    }
    return &pb.GetUserResponse{User: user}, nil
}

2.3.2 启动 gRPC 服务

func main() {
    lis, err := net.Listen("tcp", ":50051")
    if err != nil {
        log.Fatalf("failed to listen: %v", err)
    }

    s := grpc.NewServer()                 // 创建 gRPC 服务器
    pb.RegisterUserServiceServer(s, &server{}) // 注册服务实现

    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {   // 启动服务
        log.Fatalf("failed to serve: %v", err)
    }
}
关键点: 使用 grpc.NewServer() 创建服务器实例,通过 RegisterUserServiceServer 将实现注册到服务器。默认不启用 TLS,生产环境应配置安全连接。

2.4 Go 语言 gRPC 客户端快速搭建

客户端通过生成的桩代码,可以像调用本地函数一样调用远程服务。

2.4.1 创建客户端连接

package main

import (
    "context"
    "log"
    "time"

    "google.golang.org/grpc"
    "google.golang.org/grpc/credentials/insecure"
    pb "path/to/your/proto/package"
)

func main() {
    // 建立连接,insecure 表示不使用 TLS(仅开发环境)
    conn, err := grpc.Dial("localhost:50051",
        grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect: %v", err)
    }
    defer conn.Close()

    // 创建客户端桩
    client := pb.NewUserServiceClient(conn)

    // 构造请求
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()

    req := &pb.GetUserRequest{UserId: 1}

    // 调用远程方法
    resp, err := client.GetUser(ctx, req)
    if err != nil {
        log.Fatalf("could not get user: %v", err)
    }

    log.Printf("User: %+v", resp.GetUser())
}

2.5 gRPC 通信流程全景图

下面的 SVG 图展示了从 .proto 契约定义到服务端/客户端通信的完整流程。

gRPC 通信流程全景图 gRPC 通信流程全景图 1. 定义 .proto 契约 service + message 2. protoc 编译 生成 .pb.go 桩代码 3. 服务端实现 实现接口 + 启动 Server 服务端内部 • 实现 UserServiceServer • grpc.NewServer() • RegisterUserServiceServer • s.Serve(lis) 4. 客户端调用 NewUserServiceClient + RPC 调用 客户端内部 • grpc.Dial() • NewUserServiceClient • client.GetUser(ctx, req) • 获取响应 resp HTTP/2 双向流通信 Protocol Buffers 序列化

2.6 常见问题与避坑指南

问题 原因 解决方案
protoc-gen-go-grpc 未找到 未安装插件或 $PATH 未包含 $GOPATH/bin 执行 go install 并确保 $GOPATH/bin 在 PATH 中
编译后缺少 _grpc.pb.go 文件 未指定 --go-grpc_out 参数 同时使用 --go_out 和 --go-grpc_out
客户端连接报错 connection refused 服务端未启动或端口不匹配 检查服务端监听地址和客户端 Dial 地址是否一致
字段值为空(零值) proto3 中所有字段默认零值,无法区分“未设置” 使用 google.protobuf.wrappers 或自定义包装消息
生产环境注意: 示例中使用 insecure.NewCredentials() 仅用于开发测试。生产环境必须配置 TLS/SSL 证书,使用 credentials.NewServerTLSFromFile 和 credentials.NewClientTLSFromFile。

2.7 本章小结

通过本章回顾,你应该掌握了:

  • Protocol Buffers 语法:message 定义、字段编号、service 与 RPC 方法声明。
  • 代码生成:使用 protoc 编译 .proto 文件,生成 Go 桩代码。
  • 服务端搭建:实现接口、注册服务、启动监听。
  • 客户端调用:建立连接、创建客户端桩、发起 RPC 调用。