第一行是固定的定义,声明当前文件使用 proto3 的语法,可选内容只有 proto3 和 proto2,默认值是 proto2 (但默认值未来可能被
protoc
版本影响,强烈建议明确写出来)。
message 的定义有点像 Java 的 class,构成方式是:
message MessageName {
field_type field_name = field_number
field_name 是我们需要的变量名,field_type 和 field_number 则可以展开说说。
field type
Field type 是数据类型,Protobuf 虽然不是编程语言,但确实是「强类型」的,在定义结构的阶段就必须声明每一个字段的类型,运行时错误的类型会无法解析。
数据类型可以简单分成两种,分别是基本数据类型和复合数据类型。
基本数据类型就是大家熟悉的那些,官方文档给了一个详细的表格,包含了类型的说明和编译成其他语言之后的对应类型,我复制过来展示一下。
.proto Type | Notes | C++ Type | Java/Kotlin Type | Python Type | 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 | 可变长编码。负数需要更多字节,如果倾向于负值,建议使用 sint32。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
int64 | 可变长编码。负数需要更多字节,如果倾向于负值,建议使用 sint64。 | 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 | 固定 4 字节编码,大于2^28的数值比 uint32 效率高。 | uint32 | int[2] | int/long[4] | uint32 | Fixnum or Bignum (as required) | uint | integer | int |
fixed64 | 固定 8 字节编码,大于2^56的数值比 uint64 效率高。 | uint64 | long[2] | int/long[4] | uint64 | Bignum | ulong | integer/string[6] | Int64 |
sfixed32 | 固定 4 字节编码。 | int32 | int | int | int32 | Fixnum or Bignum (as required) | int | integer | int |
sfixed64 | 固定 8 字节编码。 | int64 | long | int/long[4] | int64 | Bignum | long | integer/string[6] | Int64 |
bool | bool | boolean | bool | bool | TrueClass/FalseClass | bool | boolean | bool | |
string | utf-8编码或者7-bit ASCII文本的字符串,长度小于2^32。 | string | String | str/unicode[5] | string | String (UTF-8) | string | string | String |
bytes | 长度小于2^32。 | string | ByteString | str (Python 2) | string | | | | |
bytes (Python 3) | | []byte | String (ASCII-8BIT) | ByteString | string | List | | | |
由于 Protobuf 用于通信,有时也可以先开发一侧的业务逻辑,再根据业务中需要的数据结构反过来编写 .proto 文件,那时候就可以按照上面的表把业务类型转换成 proto 的类型。
复合数据类型就不是具体的类型了,也可以叫做其他数据类型,可能是由修饰符加类型的方式构成,也可能是引用其他的 message
。
enum
的定义跟 message
有两个区别,分别是关键字要使用 enum
和 field number 要求从 0 开始。
举个栗子:
enum Type {
A = 0
B = 1
message Example {
Type type = 1
string content = 2
编译后在其他语言中使用就是普通的枚举类型了。
repeated
repeated
关键字用在 field type 之前,表示有 0 到多个该类型的内容,换句话说就是「列表」或者「数组」。
repeated
不能连续使用,也就是 Protobuf 不支持二维数组,但通过 repeated - message - repeated 的结构也能实现类似的结构,功能上并没有欠缺。
map
可以理解成 Java 的 HashMap 或者 C++ 的 std::map 之类的数据结构,可以这样定义:
map<key_type, value_type> map_field_name = field_number
map
有一些特点:
key_type 只能是浮点数之外的基本数据类型,比如 int32、string、bytes 等
value_type 可以是任意类型
map 不可以被 repeated 修饰,map 的本身就可以有 0 到多个
map 中的内容是无序的
重复的 key 只会序列化最后一个,如果从文本解析出现重复的 key 则可能失败
在 Protobuf 的使用场景中,map 和 repeated message 经常是都能实现功能的,通过 Protobuf 序列化的过程保持消息的唯一性大概率不如语言本身实现的 hashmap 效果更好。横向对比的话,功能相似的 JSON 就没有类似的结构,总的来说不是很推荐用。
oneof
oneof
是熟悉了 Protobuf 之后非常常用的结构,含义是「其中之一」,可以实现类似于泛型的效果。直接看例子:
message ConnectMessage {
string id = 1
oneof content {
string text_message = 2
ImageMessage image_message = 3
LinkMessage link_message = 4
IM 工具通信的时候,每一条消息只能有一种类型,对应着一种 UI 的显示,消息的内容就是天然的 oneof 结构。
oneof
的语法如上所示,field number 是在当前 message 顺延的,花括号并不意味着是独立的作用域(.proto 也没有作用域的概念)。
oneof
同样不可以 repeated,并且只能有一个值。比如先 set_image_message 在 set_text_message,之前的 image_message 就会被删除,如果是在 C++ 这种语言中,再使用之前拿到的 image_message 指针就会出 bug。
field number
Field number 在等号的右边,乍一看可能会被当成默认值,但实际上毫无关系,基础数据类型的默认值是跟类型绑定的。
实际上 field number 是一个「序号」,表示这个字段在序列化之后的二进制数据中的位置。序号是一个数字,取值范围是 [1, 19000) 和 (19999, 2^29 - 1],中间消失的数字是 Protobuf 保留的。实际上也很难用 19000 到这么大的数字,对于 message 来说,序列化之后的序号也会占用空间,而 [1, 15] 的序号只占用 1 字节,推荐优先使用。
「序号」机制带来的另一个问题是兼容性,随着版本迭代,.proto 文件中的字段内容也可能变化,如果某一个序号在新旧版本中有不同的类型定义,解析数据的时候就会出问题,序号和数据类型都相同而内容含义变化了的话,则可能产生更加离谱的问题。所以在更新 .proto 文件的时候,我们需要 保留(reserved) 已经使用的「序号」。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
reserved 关键字可以标记 field number 或者 field name,被标记的 field number 或者 field name 重新被使用的时候,用 protoc
编译会提示错误以及错误原因。
实际使用时为了 .proto 文件的可读性,我们不会完全按顺序使用 field number,而是在不同的逻辑分组之间留出足够多的可用数值,reserved
不算常用。
到此为止,使用 Protobuf 通信已经不成问题了,基本语法缺少了一些能力的支持,官方在 github 上给了一些补充类型。
官方补充类型:Any
链接: google/protobuf/any.proto
前面提到过,Protobuf 是「强类型」的,每个字段都需要一个类型。Any 提供了一种额外的思路,将不确定类型的内容按字节流嵌入一个 message。对于 Any 类型的生成和解析需要特殊的 API,在 Java 中是 pack
和 unpack
,在 C++ 中是 PackFrom
和 UnpackTo
。
这里的 Any 跟 Java 的 Object 或者 Kotlin 的 Any 并不相同,特别是使用场景上,Protobuf 支持 oneOf,并不需要一个 Any 来实现一个字段统合不同类型。
官方补充类型:Wrappers
链接: google/protobuf/wrappers.proto
Wrappers 的目标是解决基本类型存在不为空的默认值的问题,比如 int32 的默认值是 0,当我解析出一个 0 的时候,无法确定是真的 0 还是这个字段不存在。要解决也很简单,因为复合类型中的 message 是存在「空」的,只要把空判断交给 message 类型就可以了。Wrappers 提供了所有基本类型的「包装」message:
// 举个栗子
message Int32Value {
int32 value = 1
使用 Int32Value 代替 int32 就可以通过 has 系列的方法先判断是否存在,存在时取值得到的 0 就是真正的 0 了。
proto2 补充说明
完整文档:protobuf.dev/programming…
proto3 是 proto2 之后出现的升级版,但能力上并不是全都包含的,比如 proto3 删除了 optional
关键字、自定义默认值功能等等,不是很推荐在同一个项目中同时使用。
但是 proto3 中可以 import proto2 写好的文件(反过来则不行),升级可以不一步到位,工作量比较可控。新项目中建议直接使用 proto3,因为 proto3 更加简单明确,也有更标准的 JSON 支持。
表格如下:
特性 | XML | JSON | Protobuf |
---|
格式 | 文本 | 文本 | 二进制 |
体积 | 大 | 小 | 更小 |
序列化速度 | 慢 | 较快 | 更快 |
反序列化速度 | 慢 | 较快 | 更快 |
数据类型 | 字符串、数值、布尔值等 | 字符串、数值、布尔值等 | 强类型定义 |
运行时检查 | 无 | 无 | 有 |
跨平台支持 | 支持 | 支持 | 支持 |
感觉好像都很对,但没有测试过,下一篇会增加序列化速度和文件体积的实测对比。