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 倍,解析速度也快得多。
下面这张图能帮你快速建立整体认知:
图上可以看到:客户端通过 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,因为它是默认值。
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 命令生成代码。步骤非常清晰:
- 安装 protoc:从 GitHub 下载对应系统的二进制包,或者用包管理器(Linux 上
apt install protobuf-compiler)。 - 安装对应语言的插件:比如 Go 语言用
protoc-gen-go-grpc,Java 用protoc-gen-grpc-java。插件放到 PATH 里。 - 编写 .proto 文件:定义好 message 和 service。
- 运行 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、手动序列化完全是两种体验。不过协议本身也有学习成本,别急,后面几章我会结合实际场景帮你选型。