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 契约定义到服务端/客户端通信的完整流程。
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 调用。