Protobuf学习记录

cuixiaogang

Protocol Buffers(简称 Protobuf)是一种高效的、语言中立的序列化数据格式,广泛用于数据存储和网络通信。它通过 .proto 文件定义数据结构,并生成多种语言的代码以便操作这些数据。

安装

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# go 
# 客户端安装
# 它负责处理 Protobuf 中数据结构的 Go 语言映射,不涉及 RPC 服务。
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
# 它仅处理与 gRPC 通信相关的代码生成,依赖 protoc-gen-go 生成的消息结构体。
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
# 依赖导入
go get github.com/golang/protobuf/protoc-gen-go
go get google.golang.org/grpc/cmd/protoc-gen-go-grpc

// php
# 客户端安装
wget https://github.com/protocolbuffers/protobuf/releases/download/v3.20.0/protobuf-php-3.20.0.tar.gz
tar -zxvf protobuf-php-3.20.0.tar.gz
cd protobuf-3.20.0
./configure --prefix=/usr/local/protobuf
make
make install
ln -s /usr/local/protobuf/bin/protoc /usr/bin/
# 依赖导入(extension)
cd ext/google/protobuf
pear package
sudo pecl install protobuf-{VERSION}.tgz
# 依赖导入
composer require google/protobuf

核心语法详解

示例结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";  // 必须放在文件第一行
package example; // 生成Go代码时,默认会作为Go包的一部分

import "common.proto"; // 导入同目录下的common.proto
import public "other.proto"; // 传递导入(依赖该文件的地方也能使用other.proto的定义)

enum Gender {
GENDER_UNSPECIFIED = 0; // 必须有默认值(0),推荐命名为UNSPECIFIED
GENDER_MALE = 1;
GENDER_FEMALE = 2;
}

message Person {
// 字段格式:[修饰符] 类型 字段名 = 字段编号;
string name = 1; // 字符串类型
int32 age = 2; // 32位整数
bool is_student = 3; // 布尔值
Gender gender = 2; // 使用枚举
}

版本声明

指定使用 proto3(若不声明默认 proto2):

1
syntax = "proto3";  // 必须放在文件第一行

包声明

避免命名冲突,类似 Go 的package:

1
package example;  // 生成Go代码时,默认会作为Go包的一部分

导入其他 proto 文件

可导入其他.proto文件的定义(支持相对路径或绝对路径):

1
2
import "common.proto";  // 导入同目录下的common.proto
import public "other.proto"; // 传递导入(依赖该文件的地方也能使用other.proto的定义)

消息定义(Message)

消息是 Protobuf 的核心,用于定义数据结构,类似 Go 的结构体。

1
2
3
4
5
6
message Person {
// 字段格式:[修饰符] 类型 字段名 = 字段编号;
string name = 1; // 字符串类型
int32 age = 2; // 32位整数
bool is_student = 3; // 布尔值
}
  • 字段编号
    • 1-15 占用 1 字节(推荐常用字段用),16-2047 占用 2 字节,不可重复,一旦使用不可修改(影响序列化兼容)。因此,开发者应该为非常频繁出现的消息元素保留数字1到15.请记住将来可能添加频繁出现的元素留出一些空间。
    • 可以制定的最小字段编号是1,最大的是536870911(2^29-1)。同时,不能使用19000到19999,因为他们是为了Protocol Buffers实现保留的。
  • 修饰符(proto3 中简化):
    • 默认字段:单值(不可为null,有默认值)。
    • repeated:表示重复字段(类似 Go 的切片[]T)。
    • optional:可选字段(可显式设置为null,proto3 v3.15 + 支持)。

字段类型映射

Protobuf 类型与常用语言类型的对应关系:

.proto Java Python Go PHP
double double float float64 float
float float float float32 float
int32 int int int32 integer
int64 long int/long int64 integer/string
uint32 int int/long uint32 integer
uint64 long int/long uint64 integer/string
sint32 int int int32 integer
sint64 long int/long int64 integer/string
fixed32 int int/long uint32 integer
fixed64 long int/long uint64 integer/string
sfixed32 int int int32 integer
sfixed64 long int/long int64 integer/string
bool boolean bool bool boolean
string String str/unicode string string
bytes ByteString str (Python 2) bytes(Python 3) []byte string

枚举(Enum)

定义离散值集合,字段值必须是非负整数:

1
2
3
4
5
6
7
8
9
10
enum Gender {
GENDER_UNSPECIFIED = 0; // 必须有默认值(0),推荐命名为UNSPECIFIED
GENDER_MALE = 1;
GENDER_FEMALE = 2;
}

message Person {
string name = 1;
Gender gender = 2; // 使用枚举
}
  • 枚举默认值为第一个字段(值为 0)。
  • 可通过option allow_alias = true;允许枚举值别名。

嵌套消息

消息内部可嵌套定义其他消息:

1
2
3
4
5
6
7
8
9
10
11
message Student {
string name = 1;

// 嵌套消息
message Score {
string subject = 1;
int32 value = 2;
}

repeated Score scores = 2; // 重复字段(切片)
}

Oneof

表示多个字段中最多只能有一个被设置(类似 Go 的interface{}联合类型):

1
2
3
4
5
6
7
message Result {
oneof data {
int32 int_value = 1;
string str_value = 2;
bool bool_value = 3;
}
}
  • 设置 oneof 中的一个字段会自动清除其他字段的值。

MAP类型

定义键值对(类似 Go 的map[K]V):

1
2
3
message Config {
map<string, int32> params = 1; // key为string,value为int32
}
  • 键类型只能是标量类型(int32/string等),不能是message或enum。
  • 生成的 Go 代码中对应map[K]V。

服务定义(Service)

用于 RPC 通信,定义服务接口(需配合 gRPC 使用):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
service UserService {
// rpc 方法名(请求消息) returns (响应消息);
rpc GetUser(GetUserRequest) returns (UserResponse);
rpc ListUsers(ListUsersRequest) returns (ListUsersResponse);
}

// 请求/响应消息
message GetUserRequest {
string user_id = 1;
}

message UserResponse {
string id = 1;
string name = 2;
}

Protobuf语法选项

在 Protobuf 中,option(选项)用于配置 .proto 文件的全局行为、消息/字段的特性或代码生成规则。选项分为文件级选项消息级选项字段级选项枚举级选项服务级选项等,部分选项是 Protobuf内置的,部分是特定语言(如 Go、Java)或工具(如 gRPC)扩展的。

通用内置选项(适用于所有语言)

文件级选项

作用于整个 .proto 文件,通常放在文件开头(版本声明和包声明之后)。

选项 说明 示例
java_package 指定生成的 Java 代码的包路径(对 Go 无效) option java_package = "com.example.proto";
java_outer_classname 指定生成的 Java 外部类名(对 Go 无效) option java_outer_classname = "UserProto";
java_multiple_files 若为 true,Java 代码会按消息拆分到多个文件(默认 false) option java_multiple_files = true;
objc_class_prefix 为生成的 Objective-C 类添加前缀(对 Go 无效) option objc_class_prefix = "EX";
deprecated 标记文件为已废弃(工具可能会警告) option deprecated = true;
cc_enable_arenas 启用 C++ Arena 内存管理(对 Go 无效) option cc_enable_arenas = true;

消息级选项

作用于 message 定义,影响消息的序列化 / 反序列化行为或代码生成。

选项 说明 示例
message_set_wire_format 仅用于 proto2,指定消息使用 MessageSet 格式(不推荐) option message_set_wire_format = true;
deprecated 标记消息为已废弃 message OldMessage { option deprecated = true; }
no_standard_descriptor_accessor 禁用生成标准描述符访问器(C++/Java 用) option no_standard_descriptor_accessor = true;

字段级选项

作用于消息中的字段,控制字段的序列化规则或代码生成细节。

选项 说明 示例
deprecated 标记字段为已废弃(使用时工具会警告) int32 old_field = 1 [deprecated = true];
packed 仅 proto2 有效,对 repeated 基本类型启用打包编码(节省空间) repeated int32 ids = 1 [packed = true];
default 仅 proto2 有效,指定字段的默认值(proto3 已移除,默认值由类型决定) string name = 1 [default = "unknown"];(proto2)
json_name 指定字段在 JSON 序列化时的名称(覆盖默认的下划线转驼峰) string user_name = 1 [json_name = "username"];

枚举级选项

作用于 enum 定义,控制枚举的行为。

选项 说明 示例
allow_alias 允许枚举值使用别名(即不同名称对应相同值) enum Status { option allow_alias = true; OK = 0; SUCCESS = 0; }
deprecated 标记枚举为已废弃 enum OldEnum { option deprecated = true; }

服务级选项

作用于 service 定义,主要用于 RPC 框架(如 gRPC)。

选项 说明 示例
deprecated 标记服务为已废弃 service OldService { option deprecated = true; }

Go 语言特定选项(protoc-gen-go 扩展)

Go 语言的 Protobuf 代码生成器(protoc-gen-go)提供了一系列扩展选项,需通过导入 google/protobuf/descriptor.proto 或直接使用特定选项。

go_package(文件级)

最常用选项,指定生成的 Go 代码的包路径和包名,避免包名冲突。格式:”导入路径;包名”(包名可选,默认取导入路径的最后一段)。

1
2
3
4
5
6
7
syntax = "proto3";
package example;

// 生成的Go代码会在 ./pb 目录,包名为 userpb
option go_package = "./pb;userpb";

message User { ... }

若不指定,默认包名为 .proto 的 package 声明,可能导致冲突。

字段级 Go 选项

通过[(go.field) = { ... }] 配置字段的 Go 代码生成规则(需导入google.golang.org/protobuf/types/descriptorpb/go_field.proto)。

选项 说明 示例
name 自定义字段在 Go 中的名称(覆盖默认的下划线转驼峰) string user_name = 1 [(go.field) = { name: "UserName" }];
type 自定义字段的 Go 类型(需兼容原类型) int32 count = 1 [(go.field) = { type: "int" }];
json_tag 自定义字段的 JSON 序列化标签(覆盖默认) string name = 1 [(go.field) = { json_tag: "user_name,omitempty" }];

消息级 Go 选项

通过[(go.message) = { ... }]配置消息的 Go 代码生成规则。

选项 说明 示例
name 自定义消息在 Go 中的类型名 message User { option (go.message) = { name: "UserInfo" }; }

gRPC 相关选项(protoc-gen-go-grpc 扩展)

用于配置 gRPC 服务代码生成,需配合 service 定义使用。

服务级 gRPC 选项

通过 [(grpc.service) = { ... }] 配置服务生成规则。

选项 说明 示例
name 自定义 gRPC 服务在 Go 中的接口名 service UserService { option (grpc.service) = { name: "UserServer" }; }

方法级 gRPC 选项

通过 [(grpc.method) = { ... }] 配置服务方法的生成规则。

选项 说明 示例
idempotency_level 指定方法的幂等性级别(影响 gRPC 重试策略) rpc UpdateUser(UpdateReq) returns (UpdateResp) { option (grpc.method) = { idempotency_level: IDEMPOTENT }; }
name 自定义方法在 Go 中的函数名 rpc GetUser(GetReq) returns (GetResp) { option (grpc.method) = { name: "FetchUser" }; }

其他常用扩展选项

JSON 映射选项

控制 Protobuf 与 JSON 的互转规则(适用于所有支持 JSON 的语言)。

选项 说明 示例
json_name(字段级) 自定义 JSON 字段名(前文已提及) string user_name = 1 [json_name = "username"];
ignore_unknown(文件级) JSON 反序列化时忽略未知字段 option json_enable_ignore_unknown = true;

验证选项(第三方扩展)

通过第三方库(如 protoc-gen-validate)添加字段验证规则,需导入对应的扩展 proto。

1
2
3
4
5
6
import "validate/validate.proto";

message User {
string email = 1 [(validate.rules).string.email = true]; // 验证邮箱格式
int32 age = 2 [(validate.rules).int32.gt = 0]; // 年龄必须大于0
}

Go 语言中 Protobuf 的使用命令

1
protoc [选项] --go_out=[输出目录] --go-grpc_out=[输出目录] 输入文件.proto
  • --go_out:指定protoc-gen-go的输出目录及参数(如 Go 模块路径)。
  • --go-grpc_out:指定protoc-gen-go-grpc的输出目录及参数(若定义了 service)。
  • --proto_path(缩写-I):指定.proto文件的导入路径(默认当前目录)。