3. gRPC基础:概念、Protocol Buffers语法、服务定义、代码生成流程

gRPC 这玩意儿,我第一次接触是在做一个内部监控系统的时候。当时用 REST 做实时数据传输,轮询压力大得不行,领导说试试 gRPC……嗯,一试就回不去了。说白了,gRPC 就是 Google 搞的一套 RPC 框架,基于 HTTP/2,用 Protocol Buffers 做序列化。它跟 REST 最大的区别是:REST 面向资源,gRPC 面向服务——你定义一个服务接口,然后直接调用远程方法,感觉像调用本地函数一样。

3.1 gRPC 核心概念

我个人的理解,gRPC 的关键点就几个:HTTP/2 多路复用、Protocol Buffers 二进制传输、四种通信模式。HTTP/2 允许在一个连接上并发处理多个请求,避免了 HTTP/1.1 的队头阻塞。Protobuf 比 JSON 小 3~10 倍,解析速度也快得多。

下面这张图能帮你快速建立整体认知:

gRPC 核心架构 客户端应用 Client Stub HTTP/2 Server Stub 服务器应用 Protobuf 序列化 Protobuf 反序列化 .proto 服务定义 支持四种通信模式:Unary / Server Streaming / Client Streaming / Bidirectional

图上可以看到:客户端通过 Stub 调用服务,数据经过 Protobuf 序列化后通过 HTTP/2 传输,服务端反序列化后交给实际业务逻辑。.proto 文件是双方契约,自动生成代码。

3.2 Protocol Buffers 语法

Protobuf 的定义文件以 .proto 结尾,我通常用 proto3 版本。语法非常简单,先声明 syntax,然后定义 message 和 service。举一个例子:

syntax = "proto3";

package user;

message UserRequest {
  int32 user_id = 1;      // 字段编号 1
  string name = 2;        // 字段编号 2
  repeated string tags = 3; // 重复字段,类似数组
}

message UserResponse {
  int32 code = 1;
  string message = 2;
  UserDetail detail = 3;  // 嵌套 message
}

message UserDetail {
  int32 age = 1;
  string email = 2;
}

这里要注意几个点:

  • 字段编号 1~15 用 1 字节编码,16 以上用 2 字节。所以高频字段用小编号,能省带宽。我曾因为把一个常用字段编号设成 200,导致消息体大了不少,后来才优化回来。
  • repeated 在 proto3 中默认 packed 编码,数组元素紧挨着放,体积更小。
  • optional 关键字在 proto3 里变成了“字段默认值”机制,不再需要显式 optional,除非你开启 optional 特性(proto3 后续版本有支持)。
  • 枚举 用 enum 定义,第一个枚举值必须为 0,因为它是默认值。
个人小建议: 用 proto3 时尽量别用 required(proto3 已废弃),依赖默认值反而让兼容性更好。你想想看,旧客户端新服务端,少了某个字段也能正常工作。

3.3 服务定义

服务定义就是告诉 gRPC:你有哪些 RPC 方法。下面这个例子定义了一个简单的用户服务:

service UserService {
  rpc GetUser (UserRequest) returns (UserResponse);                // 一元
  rpc ListUsers (UserRequest) returns (stream UserResponse);       // 服务端流
  rpc UpdateUser (stream UserRequest) returns (UserResponse);      // 客户端流
  rpc Chat (stream UserRequest) returns (stream UserResponse);     // 双向流
}

gRPC 支持四种模式,区别如下:

模式请求响应适用场景
Unary(一元)单个消息单个消息常规查询/写入
Server Streaming(服务端流)单个消息流式多个消息推送日志、订阅通知
Client Streaming(客户端流)流式多个消息单个消息上传文件、批量提交
Bidirectional(双向流)流式多个消息流式多个消息实时聊天、协同编辑

注意:双向流里两端可以独立发送,顺序由应用保证。我的一个教训是:在双向流里搞了个全局锁,结果吞吐量直接掉到个位数……后来改成异步消息队列才搞定。

3.4 代码生成流程

定义好 .proto 文件后,需要用 protoc 命令生成代码。步骤非常清晰:

  1. 安装 protoc:从 GitHub 下载对应系统的二进制包,或者用包管理器(Linux 上 apt install protobuf-compiler)。
  2. 安装对应语言的插件:比如 Go 语言用 protoc-gen-go-grpc,Java 用 protoc-gen-grpc-java。插件放到 PATH 里。
  3. 编写 .proto 文件:定义好 message 和 service。
  4. 运行 protoc:
protoc --proto_path=. --go_out=. --go-grpc_out=. user.proto

这条命令会在当前目录生成 user.pb.go(消息结构)和 user_grpc.pb.go(服务接口与客户端/服务端代码)。

避坑指南: 生成的代码永远不要手动修改。我之前偷懒改了生成代码里的一个字段名,结果重新生成时全被覆盖,线上直接挂了。正确的做法是:把业务逻辑写在单独的模块里,通过接口依赖生成的桩代码。

代码生成后,服务端需要实现接口:

type server struct {
  pb.UnimplementedUserServiceServer
}

func (s *server) GetUser(ctx context.Context, req *pb.UserRequest) (*pb.UserResponse, error) {
  // 你的业务逻辑
  return &pb.UserResponse{...}, nil
}

客户端直接通过连接调用:

conn, _ := grpc.Dial("localhost:50051", grpc.WithInsecure())
client := pb.NewUserServiceClient(conn)
resp, _ := client.GetUser(ctx, &pb.UserRequest{UserId: 1})

嗯,整个流程走下来你就能感受到:gRPC 的约定大于配置,跟 REST 那种手写 URL、手动序列化完全是两种体验。不过协议本身也有学习成本,别急,后面几章我会结合实际场景帮你选型。