4. gRPC 入门:从 IDL 到流式通信
gRPC 是 Google 开源的高性能 RPC 框架,基于 HTTP/2 协议,默认使用 Protocol Buffers(protobuf)作为接口定义语言(IDL)和数据序列化格式。本章将带你从零搭建一个完整的 gRPC 服务,涵盖 protobuf 语法、编译流程、服务定义与实现,以及四种 RPC 模式中的核心两种:一元 RPC 和流式 RPC。
4.1 Protocol Buffers 语法速览
Protocol Buffers 是一种语言中立、平台中立的结构化数据序列化机制。通过 .proto 文件定义数据结构和服务接口,然后使用 protoc 编译器生成目标语言代码。
4.1.1 基本消息类型
| 关键字/类型 | 说明 | 示例 |
|---|---|---|
syntax |
指定 protobuf 版本(proto3) | syntax = "proto3"; |
message |
定义结构化数据 | message User { ... } |
int32 / int64 |
变长整数类型 | int32 age = 1; |
string |
UTF-8 字符串 | string name = 2; |
repeated |
数组/列表 | repeated string tags = 3; |
enum |
枚举类型 | enum Gender { MALE = 0; FEMALE = 1; } |
map |
键值对映射 | map<string,string> meta = 4; |
字段编号规则:每个字段必须分配一个唯一的数字编号(1 到 536,870,911)。1~15 号字段使用 1 字节编码,16~2047 号使用 2 字节编码。高频字段应优先使用小编号。
4.1.2 完整 proto 文件示例
syntax = "proto3";
package userpb;
option go_package = "github.com/example/protos/userpb";
message User {
int32 id = 1;
string name = 2;
string email = 3;
Gender gender = 4;
repeated string roles = 5;
map<string, string> attributes = 6;
}
enum Gender {
GENDER_UNSPECIFIED = 0;
GENDER_MALE = 1;
GENDER_FEMALE = 2;
}
service UserService {
rpc GetUser (GetUserRequest) returns (User);
rpc ListUsers (ListUsersRequest) returns (stream User);
}
message GetUserRequest {
int32 user_id = 1;
}
message ListUsersRequest {
int32 page_size = 1;
int32 page_token = 2;
}
4.2 protoc 编译与代码生成
使用 protoc 编译器将 .proto 文件编译为 Go 代码。需要安装 protoc 以及 Go 插件 protoc-gen-go 和 protoc-gen-go-grpc。
4.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
# 验证安装
protoc --version
protoc-gen-go --version
protoc-gen-go-grpc --version
4.2.2 编译命令
protoc --proto_path=protos \
--go_out=gen --go_opt=paths=source_relative \
--go-grpc_out=gen --go-grpc_opt=paths=source_relative \
protos/user.proto
| 参数 | 说明 |
|---|---|
--proto_path |
指定 proto 文件搜索路径,可简写为 -I |
--go_out |
生成消息类型的 Go 代码输出目录 |
--go-grpc_out |
生成 gRPC 服务端/客户端桩代码输出目录 |
paths=source_relative |
保持与 proto 文件相同的目录结构 |
生成产物:编译后得到两个文件:
user.pb.go(消息序列化代码)和 user_grpc.pb.go(服务端接口与客户端桩代码)。
4.3 gRPC 服务定义与实现
gRPC 服务定义在 .proto 文件的 service 块中。每个 RPC 方法需要指定请求消息和响应消息类型。下面我们实现一个完整的用户查询服务。
4.3.1 服务端实现
package main
import (
"context"
"log"
"net"
"google.golang.org/grpc"
pb "github.com/example/protos/userpb"
)
type userServer struct {
pb.UnimplementedUserServiceServer
}
func (s *userServer) GetUser(ctx context.Context, req *pb.GetUserRequest) (*pb.User, error) {
// 模拟数据库查询
return &pb.User{
Id: req.UserId,
Name: "Alice",
Email: "alice@example.com",
Gender: pb.Gender_GENDER_FEMALE,
Roles: []string{"admin", "user"},
}, 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{})
log.Printf("server listening at %v", lis.Addr())
if err := s.Serve(lis); err != nil {
log.Fatalf("failed to serve: %v", err)
}
}
4.3.2 客户端调用
package main
import (
"context"
"log"
"time"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
pb "github.com/example/protos/userpb"
)
func main() {
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()
user, err := client.GetUser(ctx, &pb.GetUserRequest{UserId: 1})
if err != nil {
log.Fatalf("could not get user: %v", err)
}
log.Printf("User: %+v", user)
}
4.4 一元 RPC 与流式 RPC
gRPC 支持四种 RPC 模式,本章重点介绍最常用的一元 RPC 和服务器端流式 RPC。
4.4.1 一元 RPC(Unary RPC)
客户端发送一个请求,服务端返回一个响应。这是最传统的 RPC 模式,适用于查询、更新等操作。
- 特点:请求-响应模型,类似 HTTP 的请求/响应。
- 适用场景:获取单个资源、提交表单、登录认证。
- 性能:由于 HTTP/2 的多路复用,多个一元 RPC 可以共享同一个 TCP 连接。
4.4.2 服务器端流式 RPC(Server Streaming RPC)
客户端发送一个请求,服务端返回一个消息流。适用于批量数据推送、日志流、实时通知等场景。
// 服务端流式实现
func (s *userServer) ListUsers(req *pb.ListUsersRequest, stream pb.UserService_ListUsersServer) error {
users := []*pb.User{
{Id: 1, Name: "Alice"},
{Id: 2, Name: "Bob"},
{Id: 3, Name: "Charlie"},
}
for _, user := range users {
if err := stream.Send(user); err != nil {
return err
}
}
return nil
}
// 客户端流式接收
func listUsers(client pb.UserServiceClient) {
stream, err := client.ListUsers(ctx, &pb.ListUsersRequest{PageSize: 10})
if err != nil {
log.Fatal(err)
}
for {
user, err := stream.Recv()
if err == io.EOF {
break
}
if err != nil {
log.Fatal(err)
}
log.Printf("Received user: %v", user)
}
}
流式 RPC 注意事项:
- 流式调用必须处理
io.EOF作为正常结束信号。 - 服务端流式 RPC 的
Send方法可能阻塞,需考虑背压(backpressure)机制。 - 客户端应使用
context.WithTimeout或context.WithCancel控制流式调用的生命周期。
4.5 四种 RPC 模式对比
| 模式 | 请求 | 响应 | 典型场景 |
|---|---|---|---|
| 一元 RPC | 单个消息 | 单个消息 | 查询用户、创建订单 |
| 服务器端流式 | 单个消息 | 消息流 | 日志推送、数据导出 |
| 客户端端流式 | 消息流 | 单个消息 | 文件上传、批量提交 |
| 双向流式 | 消息流 | 消息流 | 实时聊天、游戏同步 |
4.6 gRPC 通信流程总览
下图展示了一个完整的一元 RPC 调用从客户端发起请求到服务端返回响应的全流程,以及 HTTP/2 多路复用带来的性能优势。
4.7 流式 RPC 数据流示意
下图展示了服务器端流式 RPC 中,客户端发送一个请求后,服务端通过多次 Send 调用将数据逐条推送给客户端,客户端通过循环 Recv 接收直到 io.EOF。
4.8 本章小结
- Protocol Buffers 是 gRPC 的 IDL 和序列化基础,通过
.proto文件定义消息和服务。 - protoc 编译器 将
.proto文件编译为 Go 代码,生成消息类型和服务桩代码。 - 一元 RPC 是最常用的请求-响应模式,适用于大多数 CRUD 操作。
- 服务器端流式 RPC 适用于批量数据推送场景,客户端通过循环
Recv()接收数据流。 - gRPC 基于 HTTP/2 协议,天然支持多路复用、头部压缩和双向流,性能优于传统 RESTful API。
实践建议:在微服务架构中,建议将
.proto 文件统一管理在独立的 Git 仓库中,作为团队间的契约标准。使用 buf 工具链可以进一步简化 proto 文件的编译、lint 和版本管理。