简介
概况
- 谷歌出品
- 语言无关,平台无关,可扩展
- 序列化/反序列化数据
版本
proto2 vs. proto3 对比:
- 特性区别
- 默认值处理:pb2可设置默认值;pb3的默认值仅能根据type确定,无法自行设置
- 性能差异很小,约1%
支持语言
C++, Java, Python, C#, JavaScript, Ruby, Go, PHP, Dart
使用
安装
- 方式一:编译安装
- 方式二:直接安装protoc (推荐)
快速安装步骤:
- 到 github.com/protocolbuf… 下载 protoc-x.x.x-osx-x86_64.zip 并解压;
- 执行以下命令
cd protoc-x.x.x-osx-x86_64
cp -r include/ /usr/local/include/
cp -r bin/ /usr/local/bin/
- 测试命令
protoc
执行后看到帮助信息,代表安装成功
编译
protoc ./xx.proto --js_out=import_style=commonjs:.
--js_out的语法:
--js_out=[OPTIONS:]output_dir,多个options间用逗号分隔
依赖
protoc编译生成的代码依赖了google-protobuf
npm install google-protobuf
方法集
在编译出的js代码中,提供了一整套方法,重要的有:
- Msg.deserializeBinary
- Msg.prototype.toObject
- Msg.prototype.serializeBinary // 序列化为Uint8Array
- 字段操作:
- 单一标量值
- getFoo
- setFoo
- hasFoo
- clearFoo
- repeated
- getFooList
- setFooList
- addFoo
- clearFooList
- bytes类型
- setFoo // 接受base64编码值或Uint8Array
- getFoo
- getFoo_asB64()
- getFoo_asU8()
- 单一标量值
.proto文件
syntax = "proto2";
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3;
}
字段id (Field Number)
用于在二进制消息格式中唯一标识字段,并且当消息类型开始使用后,字段id就不能再更改了。
范围是 1~2^29-1(536870911)
19000~19999是保留数字,用于protobuf的实现,用户不可使用。
被标记为 reserved 的数字也不可使用。
编码时,1-15只占用1个字节,16-2047占用2个字节。设计消息体时,要注意:
- 1-15号留给最常使用的字段
- 要留一些位置给将来可能添加的常用字段
字段可选性
required: 必须包含一个本字段optional: 可以不含 或者 包含一个本字段repeated: 可包含0~任意个本字段,且值的顺序会被保存 由于历史原因,repeated的数字类型编码效率不高。当数字类型(varint, 32-bit, or 64-bit)时,可以使用 [packed=true] 来启用更高效的编码:
repeated int32 samples = 4 [packed=true];
关于 required 的争议:一旦将字段设为required后,就不能改成非必选了,因为旧reader会认为它不完整。一种实践是只使用optional和repeated,然后在具体代码中检查对必选字段进行检查。这种实践在谷歌内部也有争议,并未得到统一。
reserved字段id
比如之前用过2,但后来又删掉了。为了防止后来又用2,可以把2设为reserved:
message Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "foo";
}
可以设置字段id,也可以设置字段名。且两者不能在同一个reserved声明中。
默认值
当消息中没包含optional字段值时,可以使用其默认值:
optional int32 result_per_page = 3 [default = 10];
若未声明默认值,则使用类型相关的默认值:
- string:空字符串
- bytes: 空字节串
- bools: false
- 数字:0
- enum: 定义中的第一项
enum
message SearchRequest {
enum Corpus {
WEB = 1;
IMAGES = 2;
VIDEO = 3;
}
optional Corpus corpus = 1 [default = WEB];
}
如果其他message内想使用Corpus,可以使用_MessageType_.EnumType, 即 SearchRequest.Corpus
enum可以允许别名,即多个名称指向同一个值:
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
使用其他message
message SearchResponse {
repeated Result result = 1;
}
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
导入:
import "myproject/other_protos.proto";
在a.proto中导入b.proto后,可以直接使用b.proto中的所有message。
嵌套message
message SearchResponse {
message Result {
required string url = 1;
optional string title = 2;
repeated string snippets = 3;
}
repeated Result result = 1;
}
message SomeOtherMessage {
optional SearchResponse.Result result = 1;
}
修改message
注意事项:
- 不要修改已存在的字段id
- 新增字段应该是optional或repeated
- 非required字段可以删除,只要它的字段id不再被使用
- int32,uint32,int64,uint64,bool 这些类型互相兼容,可直接修改
- sint32和sint64互相兼容,但这两个和其他数字类型不兼容
- 当bytes使用utf-8时,string和bytes是兼容的
- fixed32和sfixed32兼容,fixed64和sfixed64兼容
extensions
a.proto
message Foo {
// ...
extensions 100 to 199;
}
b.proto
import "./a.proto";
extend Foo {
optional int32 bar = 126;
}
package包名
在.proto文件中可以声明本文件的包名: a.proto
package foo.bar;
message Open { ... }
b.proto
import "./a.proto";
message Foo {
...
required foo.bar.Open open = 1;
...
}
类型
int64(变长编码) 和 fixed64(定长编码) 对比:
局限性
由于js的Number基于IEEE 754标准,最大安全整数为 2^53 - 1 (Number.MAX_SAFE_INTEGER),无法精确表示到int64中较大的数字(例如:2^54)。实验:
编码
varint编码
varint是用于整型数字的一种编码方式。它用一个或多个字节表示一个数字,较小的数字使用更少字节表示。
编码过程: 300 ↓二进制 100101100 ↓补齐为7的倍数位 00000100101100 ↓7位分组 0000010 0101100 ↓插入msb 00000010 10101100 ↓倒序 10101100 00000010 ↓Uint8Array [172, 2]
解码过程: [172, 2] ↓ 二进制 10101100 00000010 ↓倒序 00000010 10101100 ↓去掉msb 0000010 0101100 ↓连接 00000100101100 ↓转为十进制 300
message结构编码
tag的值为:(field_number << 3 | wire_type)
计算出值后,将值使用varint编码,再塞入message的二进制数据中。
由此可见,tag中包含了 字段id 和 wire_type 两个信息。wire_type有:
举个解码的例子:
例如字节流 0x08 0x96 0x01 的解码过程为:
varint tag是0x08, 二进制为 0000 1000, 去掉msb为 000 1000, 分组为 0001 000,由此可知:
- field_number = 1
- wire_type = 0 = varint类型 知道后面是varint类型了,就可以解码了: 0x96 0x01 字节流 1001 0110 0000 0001 二进制 0000 0001 1001 0110 倒序 000 0001 001 0110 去掉msb 1001 0110 连接 150 十进制
有符号整数
当编码负数时,有符号整数(sint32/sint64)和“普通”整数(int32/int64)有区别:
- 使用int32/int64编码负数时,结果的varint编码总是要用10字节来表示,它实际时一个很大的无符号整数,因为要使用最高位表示符号
- 如果使用sint32/sint64,编码时先使用ZigZag编码将整数映射为一个无符号整数,在使用varint编码。
ZigZag编码
ZigZag编码将有符号整数映射为无符号整数,绝对值较小的数字的编码值也比较小(例如-1被映射为1)。它在负数和正数之间“之字形”跳动来实现这一点:
编码计算公式:
sint32: (n << 1) ^ (n >> 31)
sint64: (n << 1) ^ (n >> 63)
含义:
- 左移1位可以消去符号位,低位补0
- 有符号右移31位将符号位移动到最低位,负数高位补1,正数高位补0
- 按位异或 对于正数来说,最低位符号位为0,其他位不变 对于负数,最低位符号位为1,其他位按位取反
解码计算公式:
(n >>> 1) ^ -(n & 1)
含义:
- 无符号右移1位
- 按位与1,然后取负值,这一步非常巧妙,对于正数就是0,负数就是-1
- 按位异或得到结果 正数是与0按位异或 负数是与-1按位异或
编码公式中向右移位的部分是算术位移,其结果:
- n是正数时,结果的所有bit全为0
- n是负数时,结果的所有bit全是1
非varint数字
- double 和 fixed64: wire type是1,占用64位
- float 和 fixed32: wire type是5,占用32位 两者都是小端字节序存储的,即高位在后,低位在前。
说protobuf压缩数据没有到极限,原因就在这,因为float/double这些浮点类型没有压缩。
字符串
字符串采用Tag-Length-Value形式编码,wire type为2,编码后,在key值后会有一个数字表示数据值的字节长度。例如:
message Test2 {
optional string b = 2;
}
将b的值设置为"testing",得到:0x12 0x07 0x74 0x65 0x73 0x74 0x69 0x6e 0x67 0x12: key值,由(2 << 3) | 2 -> 0x12 计算而来 0x07: 数据值字节长度是7 0x74 ... 0x67: “testing”的utf-8编码
嵌套message
编码方式与字符串类似,采用Tag-Length-Value形式。
优缺点
在序列化方面,protobuf相比XML,有如下优点:
- 数据体积小
- 更快的反序列化速度
- 可自动化生成数据访问类
缺点:
- 以二进制方式存储,可读性几乎为0
- 必须有.proto定义,才能够解析
- 不适合大message处理,一般超过1M字节的message就应该考虑其他方案了
一些FAQ
为什么没有proto1
早在2001年,google内部就开始protobuf的开发了,最初版本就是proto1。经过多年演变,很多使用它的开发者想到什么特性就自己向里面添加了,这就造成代码很混乱。随后他们意识到这样开发项目是不可行的。 于是在proto2中进行了完全的重写,代码组织更加合理了,而且也完全不会依赖任何一款google的未开源项目。
为什么叫“Protocol Buffers”?
这个名字起源于最初的版本,当时还没有protoc编译器帮我们生成class代码。当时是真的有个类叫 ProtocolBuffer的类用作缓冲区,用户可以通过类似 AddValue(tag, value)的方法添加键值对数据并保存为字节数据,并且最后可以一次性将所有数据输出。 后来这名字就一直沿用了。
其他实现:protobufjs
一套开源的protobuf的js实现 github.com/protobufjs/…
命令
- pbjs:根据.proto文件生成json描述符/静态代码
- pbts:根据静态代码生成.d.ts文件
特性
- 支持直接加载.proto/.json
- 库版本 full/light/minimal,分别对应.proto/json/static code
- 校验数据类型
long.js
js中安全整数最大值是2^53-1 (Number.MAX_SAFE_INTEGER); js的位运算只处理[-2^31, 2^31 - 1] 或 [0, 2^32 - 1]范围内的整数; protobufjs处理大数时使用了long.js,突破了IEEE 754的限制,它能够处理大于2^53-1的整数,且支持64位整数的位运算
相关文档
官方文档:developers.google.com/protocol-bu…
pb介绍:halfrost.com/protobuf_en…
pb序列化:halfrost.com/protobuf_de…
IEEE754: zh.wikipedia.org/wiki/IEEE_7…