目录
前言
一、Protobuf 基本语法
1.1、Protoc 版本
1.2、文件格式配置
1.3、消息字段规则
1.3.1、字段数据类型
1.3.2、字段修饰规则
1.3.3、消息类型定义
1.3.4、enum 类型
1.3.5、Any 类型
1.3.6、oneof 类型
1.3.7、map 类型
1.3.8、默认值
1.3.9、更新消息规则
1.3.10、保留字段 reserved
1.3.11、选项 optional(了解,proto3 移除)
前言
前面在讲 gRPC 的时候有讲到 Protobuf 的语法,但实际上远没有这么简单,有很多坑和注意事项,所以这篇文章就是来补坑的~
一、Protobuf 基本语法
1.1、Protoc 版本
1.2、文件格式配置
a)创建文件:文件都是 proto 为后缀,例如 UserService.proto
b)基本内容:
// 设定使用的 proto 版本
syntax = "proto3";/**java_multiple_files = true 表示 Protobuf 编译器会为每个定义的消息类型生成一个单独的 Java 文件,而不是都放在一个文件中java_multiple_files = false 表示 Protobuf 编译器会把所有的消息类型生成的 Java 代码都放在一个文件中*/
option java_multiple_files = false;/**指定 protobuf 生成的类,放在哪个包中*/
option java_package = "org.cyk";/**指定 protobuf 生成的外部类的名字外部类是用来管理内部类的内部类才是开发中使用的*/
option java_outer_classname = "HelloProto";
c)导包:例如有 A.proto 和 B.proto 文件,现在需要在 B.proto 文件中引入 A.proto 文件的内容,就需要使用 import
import xxx/A.proto
1.3、消息字段规则
1.3.1、字段数据类型
消息中定义的数据类型(我们主要关心 .proto 对应的 Java/Kotlin 类型).
以下列表来自官网:Language Guide (proto 3) | Protocol Buffers Documentation
.proto Type | C++ Type | Java/Kotlin Type[1] | Python Type[3] | Go Type | Ruby Type | C# Type | PHP Type | Dart Type | |
---|---|---|---|---|---|---|---|---|---|
double | double | double | float | float64 | Float | double | float | double | |
float | float | float | float | float32 | Float | float | float | double | |
int32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | |
int64 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | |
uint32 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int | |
uint64 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | |
sint32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | |
sint64 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | |
fixed32 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int | |
fixed64 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 | |
sfixed32 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int | |
sfixed64 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 | |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String | |
bytes | string | ByteString | str (Python 2) bytes (Python 3) | []byte | String (ASCII-8BIT) | ByteString | string |
1.3.2、字段修饰规则
消息字段可以使用如下规则修饰:
- singular(proto3 中默认使用此规则):表示该字段只能 null 或者是一个具体值.
- repeated:表示为 Java 中的 List 类型.
例如如下:
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";message Userinfo {string name = 1; // 这里的数字是字段的唯一标识,因为将来时面向字节流传输,需要让每个字段能够对应的上int32 age = 2;repeated string phone = 3;
}
1.3.3、消息类型定义
在单个 .proto 文件中可以定义多个消息,并且支持嵌套类型,不同消息体重的编码可以重复.
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";//1.非嵌套
message Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;
}//2.嵌套
message Userinfo2 {string name = 1;int32 age = 2;repeated string phone = 3;message Stat {int32 rank = 1;int32 fans = 2;}
}//3.消息类型可作为字段使用
message Human {string aaa = 1;Userinfo1 userinfo = 2;
}
1.3.4、enum 类型
enum ArticlePubType {NORMAL = 0; //发布普通文章PRIVATE = 1; //发布私有文章TIMER = 2; //定时发布文章
}
规则如下:
- 第一个枚举值必须是 0.
- 枚举类型可以定义在消息外,也可以在定义在消息体内(嵌套).
- 枚举的常量值在 32 位整数范围内. 但因为负值无效,所以不建议使用(和编码规则有关).
- 同级(同一个文件下,或者是引入的其他 proto 文件)枚举类型,枚举值不能重名.
1.3.5、Any 类型
可以简单的理解位 泛型. 因此 Any 中可以存储任意消息类型. 并且 Any 类型也可以使用 repeated 修饰.
Note:Any 类型是 google 已经定义好的类型,在 include 目录下就可以找到所有 google 已经定义好的 .proto 文件.
import "google/protobuf/any.proto"; //引入 Anymessage Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;google.protobuf.Any data = 4;
}
将来通过 protoc 编译生成的 Java 文件中,给 Any 类型生成的对象提供了如下方法:
- hasXXX:用来检测当前字段是否被设置(这个方法存在的意义在于,字段即使不设置也是有默认值的,因此 has 就可以检测到底是否真的有设置值).
- setXXX:要求传一个 Any 类型的对象.
- 对于 Any 类型:
- Any.pack(T message) 可以讲任意消息类型转化成 Any 类型.
- message.getAny().unpack(Class<T> clazz) 方法可以将 Any 类型转回之前设置的任意消息类型.
- message.getAny().is(Class<T> clazz) 用来判断存放的消息类型.
1.3.6、oneof 类型
如果消息中有很多可选字段,并且将来只有一个字段会被设置,那么就可以使用 oneof 来约束这个行为.
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";import "google/protobuf/any.proto"; //引入 Anymessage Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;google.protobuf.Any data = 4;oneof other_content {string qq = 5;string wechat = 6;}
}
注意事项:
- 可选字段中的 字段编号 不能和 非可选字段 的编号冲突.
- oneof 中不能使用 repeated 字段.
- 如果将来 oneof 中有多个字段被设置了值,那么只会保留最后一个设置的成员,之前设置的 oneof 成员会自动清除.
oneof 将来生成 Java 代码中会提供以下方法:
- clear():清空 oneof 中的字段.
- getXXXCase():获取当前设置了哪些字段.
- hasXXX():检查当前字段是否被设置.
1.3.7、map 类型
类似于 Java 中的 HashMap,使用方式如下:
map<key_type>, value_type> map_field = N;
注意:
- key_type 是除了 float 和 bytes 类型以外的任意标量类型.
- value_type 可以是任意类型.
- map 字段不可以用 repeated 修饰.
例如:
syntax = "proto3";option java_multiple_files = false;
option java_package = "org.cyk";
option java_outer_classname = "UserProto";import "google/protobuf/any.proto"; //引入 Anymessage Userinfo1 {string name = 1;int32 age = 2;repeated string phone = 3;google.protobuf.Any data = 4;oneof other_content {string qq = 5;string wechat = 6;}map<string, string> arguments = 7;
}
1.3.8、默认值
如果将来给服务端发送的消息对象中有一些字段没有设置值,那么将来这些消息字段在反序列化时就会被设置默认值. 不同类型默认值不同:
类型 | 默认值 |
---|---|
字符串 | 空字符串 |
字节 | 空字节 |
布尔值 | false |
数值类型 | 0 |
枚举 | 默认是第一个定义的枚举值,也就是 0(也必须是 0). |
消息字段 | 具体根据语言而定 |
repeated 修饰的字段 | 空列表 |
消息字段、oneof字段、any字段 | 有 has 方法来检测当前字段是否被设置 |
1.3.9、更新消息规则
当现有消息类型已经不再满足我们的需求,需要扩展一个字段的时候,要注意遵守以下规则:
a)禁止修改任何已有的字段编号.
例如你更新的这个 proto 文件中一个已有的字段编号,并且这个新版的 proto 文件被用于生成序列化代码,但是服务端这边反序列化代码还是使用的旧代码进行反序列化,这就可能导致数据丢失或者解析错误.
b)如果要删除老字段,要保证不再使用删除字段的编号. 正确的做法是通过 reserved 保留字段编号,确保该编号不能重复使用.
因为如果我们只是简单的删除了某一个字段而不采取其他措施,那么就可能导致和 (a) 一样的情况.
c)int32、uint32、int64、bool 之间是完全兼容的. 也就是说这些类型中任意一个改成另一个,都不会破坏兼容性. 不过还是要注意,如果从精度较高的字段转化为精度较低的字段可能会被截断.
例如 64位 当作 32位 读取,虽然不存在兼容性问题,但会截断 32 位.
d)新增一个字段到 oneof 类型是不安全的.
这个本质上 和 (a) 问题一致. 因为 oneof 如果被客户端设置的字段是新增的字段,而服务端这边还是使用旧的反序列化代码解析,就可能会出现问题.
1.3.10、保留字段 reserved
如果通过删除字段来更新消息,未来用户在添加新字段时,可能会使用以前被删除的字段编号. 将来使用 proto 旧版本的程序就会引发很多问题.
那么为了确保不会发生这种情况的方法就是:使用 reserverd 将指定字段的 编号 或 名称 设置位保留项. 当我们再使用这些 编号 或 名称 时,protocol 编译器将会警告这些编号不可用.
1.3.11、选项 optional(了解,proto3 移除)
在 proto2 中 optional 是一个字段修饰符,标识字段在消息中是可选的(消息中可能包含该字段,也可能不包含). 但是从 proto3 开始,optional 就被移除了,并且所有字段都是可选的(因为他们都有默认值).