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 多路复用带来的性能优势。

gRPC 一元 RPC 通信流程 gRPC 一元 RPC 通信流程 gRPC 客户端 业务代码 gRPC Stub HTTP/2 客户端 请求 gRPC 服务端 HTTP/2 服务端 gRPC Server 服务实现 响应 HTTP/2 多路复用 • 单一 TCP 连接 • 双向并发流 • 头部压缩 (HPACK) • 服务器推送 • 流优先级控制 • 减少延迟与带宽 Protobuf 序列化 • 二进制编码,体积小 • 解析速度快 • 向前/向后兼容 • 强类型约束

4.7 流式 RPC 数据流示意

下图展示了服务器端流式 RPC 中,客户端发送一个请求后,服务端通过多次 Send 调用将数据逐条推送给客户端,客户端通过循环 Recv 接收直到 io.EOF。

服务器端流式 RPC 数据流 服务器端流式 RPC 数据流 客户端 发送请求 循环 Recv() 处理 EOF 服务端 接收请求 循环 Send() 返回 EOF 请求消息 数据流 1 数据流 2 数据流 N 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 和版本管理。