Protobuf 协议
Protobuf 介绍
Protocol Buffers (protobuf) 是由 Google 开发的语言中立、平台无关的可扩展机制,用于序列化结构化数据,通常用于通信协议、数据存储等场景。Protobuf 提供了一种紧凑的二进制格式,在多语言、多平台环境下非常高效,尤其在性能要求较高的分布式系统、微服务架构和 RPC 调用中非常常见。
Protobuf 的优点
- 高效:相比于 JSON 和 XML,Protobuf 的二进制格式更加紧凑,占用更少的带宽和存储空间。
- 跨语言支持:Protobuf 支持多种 语言,包括 C++、Java、Go、Python 等,使得它适合多语言的项目。
- 版本兼容:Protobuf 的消息定义支持向后兼容,便于在不影响现有客户端的情况下更新消息格式。
Protobuf3 基本语法
核心的概念
- message: 类似于类或结构体,定义了数据的结构。
- field: message 中的每个字段有一个唯一的标识符(tag),用于序列化时的定位。
- service: 用于定义 RPC 服务,proto3 支持与 gRPC 集成。
- import: 用于导入其他
.proto
文件,可以解决依赖关系。 - option: 用于设置编译选项,例如生成的 Go 包名。
基本数据类型
- int32, int64: 有符号整型
- uint32, uint64: 无符号整型
- bool: 布尔类型
- string: 字符串类型
- bytes: 字节类型
- float, double: 浮点数类型
- enum: 枚举类型
一个 protobuf 文件示例 user.proto
syntax = "proto3";
package user;
option go_package = "user";
service UserService {
rpc GetUser (GetUserRequest) returns (User);
}
message GetUserRequest {
int32 id = 1;
}
// 定义用户信息的 message
message User {
int32 id = 1;
string name = 2;
string email = 3;
repeated PhoneNumber phones = 4;
}
// 定义电话类型
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
// 枚举电话的类型
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
Protobuf 使用 .proto
文件来定义数据结构和 RPC 服务,随后需要使用 protoc
编译器将其编译成目标语言代码。你可以从 Protobuf 官方 GitHub 资源库 下载并安装适用于各平台的 protoc
编译器。
在 sponge 使用 Protobuf 规范
sponge 内置了基于 protobuf3生成 http/gRPC 的 API 接口代码插件,可根据业务需求灵活调整生成代码。
在 sponge 创建的服务中,proto 文件统一存放在 api 目录下,目录路径为:api/<服务名称>/<版本号>/proto 文件
,例如:api/user/v1/user.proto
。
.
├── api # 存放 proto 文件目录
│ └── user # 服务名称
│ └── v1 # 版本号
│ └── user.proto # proto 文件
└── third_party # 第三方引用的 proto 文件
以一个用户服务为例,go module 名为edusys
,例如 user.proto 文件内容如下,
syntax = "proto3";
package api.user.v1;
option go_package = "edusys/api/user/v1;v1";
import "api/user/types.proto";
import "validate/validate.proto";
service UserService {
rpc List(ListUserRequest) returns (ListUserReply) {};
}
message ListUserRequest {
api.user.v1.Params params = 1 [(validate.rules).message.required = true];
}
message ListUserReply {
int64 total = 1;
repeated User users = 2;
}
message User {
int32 id = 1;
string name = 2;
string email = 3;
}
package 规范
package
的定义需要与 api 目录的路径对应,命名格式为api.<服务名称>.<版本号>
,例如api.user.v1
。
go_package 规范
go_package
的定义也需要与 api 目录的路径对应,命名格式为<go module 名称>/api/<服务名称>/<版本号>;<版本号>
,例如edusys/api/user/v1;v1
。
import 规范
依赖本服务中的其他 proto 文件,找到依赖的 proto 文件(例如 types.proto),查看该文件的package
名称(例如 api.user.v1),然后在当前 proto 文件中import "api/user/types.proto";
。
依赖第三方的 proto 文件third_party
目录下的 proto 文件,找到依赖的 proto 文件(例如 annotations.proto),查看该文件的package
名称(例如 google.api),然后在当前 proto 文件中import "google/api/annotations.proto";
。
message 规范
如果 proto 文件只用于 gRPC 服务,只需看注意以下规范点1、2、3。
如果 proto 文件用于 Web API 接口,则需注意以下规范点1、2、3、4、5、6。
建议使用驼峰命名法为消息字段命名,例如:firstName、lastName 等。
如果消息字段名称以“id”结尾,建议使用 xxxID 的命名格式,例如:userID、orderID 等。
添加验证规则 https://github.com/envoyproxy/protoc-gen-validate#constraint-rules,例如:
uint64 id = 1 [(validate.rules).uint64.gte = 1];
如果路由包含路径参数,例如
/api/v1/userExample/{id}
,则定义的消息必须包含路径参数的名称,并且该名称应添加一个新标签,例如:int64 id = 1 [(tagger.tags) = "uri:\"id\""];
如果请求 URL 后面带有查询参数,例如
/api/v1/getUserExample?name=Tom
,在消息中定义查询参数时必须添加表单标签,例如:string name = 1 [(tagger.tags) = "form:\"name\""];
如果消息字段名称包含下划线(例如 "field_name"),会导致 Swagger 请求参数的 JSON 字段名称与 GRPC 的 JSON 标签名称不同。对此有两个解决方案:
方案 1:移除消息字段名称中的下划线。
方案 2:使用第三方工具 protoc-go-inject-tag 来修改 JSON 标签名称,例如:
string first_name = 1; // @gotags: json:"firstName"
google.api.http selector 字段自定义规范
selector
字段原来用于设置 HTTP 方法到 RPC 方法的映射关系,但在 sponge 不需要用到映射关系,因此我们将其扩展为用来定义生成对应 gin 相关的代码,扩展值为[ctx]
或[no_bind]
。
[ctx]
:表示这个路由透传 gin.Context 到 handler,内部仍然会执行 ShouldBinding 参数,proto 文件示例:// 登录 rpc Login(LoginRequest) returns (LoginReply) { option (google.api.http) = { post: "/api/v1/auth/login" body: "*" selector: "[ctx]" }; }
- 在 handler 获取 gin.Context,示例:
// Login 登录 func (h *userHandler) Login(ctx context.Context, req *userV1.LoginRequest) (*userV1.LoginReply, error) { c, ctx := middleware.AdaptCtx(ctx) // 变量 c 是 gin.Context // ...... }
[no_bind]
:表示这个路由设置了透传 gin.Context,并且跳过了 ShouldBingding 参数,proto 文件示例:// 上传文件 rpc Upload(UploadRequest) returns (UploadReply) { option (google.api.http) = { post: "/api/v1/upload" body: "*" selector: "[no_bind]" }; }
- 在 handler 获取 gin.Context,示例:
// Upload 上传文件 func (h *userHandler) Upload(ctx context.Context, req *userV1.UploadRequest) (*userV1.UploadReply, error) { c, ctx := middleware.AdaptCtx(ctx) // 变量 c 是 gin.Context // 处理上传文件逻辑 }
sponge 结合 protobuf3 的优势,用于 Web、RPC 系统、微服务和分布式系统中。通过定义简单的 .proto
文件,开发者可以自动生成序列化和反序列化代码,极大地减少手动编码的工作量,并提高代码的性能和可维护性。
References: