protobuffer的前世今生(二)——编码
一个简单的message
首先看一个简单的消息定义:
message Test1 {
optional int32 a = 1;
}
在一个应用中,你创建一个Test1
message 并且设置 a为150.你可以序列化这个消息到输出流,你可以得到3个字节
08 96 01
到此为止,如此之小,如此数字化——但是这意味着什么呢?请继续读下去…
base 128 Varints
为了简单理解pb的编码,你首先要理解varints。varints 是一个使用一个或多个字节来序列化整数的方法。小数字则使用字节少。
所以,举个栗子,这里有一个数字1——这个是一个单字节,所以编码如下:
0000 0001
300 如下——这个稍微复杂点:
1010 1100 0000 0010
你如何知道这就是以代表300呢?
首先,你需要去掉每个字节的最高有效位,因为这个最高有效位只是告诉我们是否到了数字的末尾(如你所见,它设置在第一个字节,因为使用varint编码300不止一个字节)
1010 1100 0000 0010
→ 010 1100 000 0010
接下来你反转 这两组7位的bit,记着,varint 存数字是首先存意义最小的那组(相比最高有效位)。 然后将他们连接起来得到如下的结果:
000 0010 010 1100
→ 000 0010 ++ 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300
Message 结构
众所周知,一个pb message 就一系列的key-value对。二进制版本的message就是用字段的编号作为key.
字段的名字和声明类型呢,是根据message类型定义文件,例如.proto文件定义反编码完毕决定的。
当一个message被编码候,key - value 对就会被连接起来成为一个字节流。当message被解码时,解析器需要跳过不认识的字段。这样,新的字段可以被添加进消息,而无需更改老的代码(老代码不认识新字段)。一个key既要对应.proto文件中定义的数字,也要对应这个字段的类型,以便决定这个value 的长度。
wire type
类型如下:
Type |
Meaning |
Used For |
---|---|---|
0 |
Varint |
int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 |
64-bit |
fixed64, sfixed64, double |
2 |
Length-delimited |
string, bytes, embedded messages, packed repeated fields |
3 |
Start group |
groups (deprecated) |
4 |
End group |
groups (deprecated) |
5 |
32-bit |
fixed32, sfixed32, float |
在message流中每个key都是一个varint类型的,它的值是 字段的数字左移3位,再或上wire type,换句话说,字节的最后三位存储的是wire type。 公式如下:
(field_number << 3) | wire_type
回过头来,再看下之前那个例子。 流中的第一个数字总是一个varint key,如下的是08(去掉最高有效位)
000 1000
从后三位得知,wire type 等于 0 ,然后向右移动3位得到字段编号,等于1.
结合之前的知识,分析 01 96 01
这三个字节存储的内容为:
96 01 = 1001 0110 0000 0001
→ 000 0001 ++ 001 0110 (去掉最高有效位,然后反转7位的一组的bits)
→ 10010110
→ 128 + 16 + 4 + 2 = 150
二进制数据流
再次结合上面分析的可以得到这个表格
更多的值类型
有符号整数
刚刚我们看到,wire type 为0 的都作为varints进行编码。
然而,有符号int 类型(sint32 and sint64
)和“标准的”int类型(int32 and int64
)
有很大的不同,尤其是遇到编码负数的时候。
如果使用int32 and int64
类型进行编码负数的话,varint的结果往往是10个字节的那么长——被当做一个非常大的无符号整数来对待。
如果使用有符号类型的话,结果会使用ZigZag进行编码,这个有效的多。
ZigZag 编码存在一个有符号整数到无符号整数的映射。因此,绝对值小的数(如-1)
也会有一个比较小的varint编码值。就这样“zig-zags”来回穿梭于负数和正数之间。
因此,-1被编码为1,1被编码为2.2被编码为4。
列一个表格,我们会发现规律。
Signed Original |
Encoded As |
---|---|
0 |
0 |
-1 |
1 |
1 |
2 |
-2 |
3 |
2147483647 |
4294967294 |
-2147483648 |
4294967295 |
… |
… |
n |
2n |
-(n+1) |
2n+1 |
其中n为自然数(即,0和正整数) 换句话说,每个值n使用了如下公式进行编码: 对sint32位的:
(n << 1) ^ (n >> 31) ①(注意是有符号右移)
对64位的sint64:
(n << 1) ^ (n >> 63) ②
可以对2这个值举个栗子: 2=0010 采用公式①
(28个0)....0100 ^ 0000...(32个0)
=0100
=4
注意,在右移31位的这部分,这个移动的结果要么是一个全0的数字(如果n是正数),要么是一个全是1的bits(如果n是负数)
当解析sint32 或 sint64
这类数值时,就会解码回有符号的版本,如-1。
非-varint 的数(非固定int类型的数)
非 varint 的数类型很简单——double
或者fixed64
的wire type 为1,这就告诉了解析器期望的是一个固定的64位的块数据,类似的,float
和fixed
的wire type 为5,这就告诉了解析器期望的是一个32位的数据。在这两种情况下,值都是以小端字节序列进行存储的。
Strings
wire type 为2 的(长度限定的),意味着值 是一个varint编码的长度后面紧跟着指定的字节数的长度数字。举个栗子:
message Test2 {
optional string b = 2;
}
如果将b的值设置为“testing”,那么结果为:
12 07 74 65 73 74 69 6e 67
红色的字节是UTF8格式的"testing",这里key就是0x12, 0001 0010 由前面知识可以得到: wire type = 2 (后三位) 字段编号 = 2. 值中的int长度指定为7位(黑色的加粗部分),你瞧,我们这不就发现了后面有7个字节(红色部分)——我们的string。
嵌入的Messages
举个栗子,Test3 消息体内,嵌入我们上面定义的Test1:
message Test3 {
optional Test1 c = 3;
}
这个是编码后的结果,再次与之前的a呼应,我们 还是设置a字段为150。
1a 03 08 96 01
如你所见,
后三个字节 和我们之前第一个例子中的一模一样(08 96 01
),在前面有一个03
——嵌入式的消息和strings的计算方法一致。
1a = 0001 1010
wire type =2 字段编号 = 0001 1010->右移三位 = 3 。符合我们的定义。
可选的 和重复的元素
在proto2中,message定义有repeated
元素(没有[packed=true]
选项),编码的message有0个或多个key-value对,它们拥有相同的字段编号。这些重复的值没有必要连续出现,他们可能和其他字段交错出现。
在解析的时候,每个元素相对的顺序是可以得到保留的。
在proto3中重复的元素使用packed编码。
在proto3中对于非重复的字段,或者在proto2中的optional
字段,编码的消息可能有也有可能没有那个字段编码的key-value对。
一般地,一个编码的消息,对于一个非重复的字段,绝不会有超过一个实例。然而,人人都有犯错的时候,我们也期望解析器能够处理这种情况。
- 对于数字类型和字符串类型的字段,如果相同的字段出现多次,解析器只接受最后看到的那个字段的值。
- 对于嵌入的message字段,相同字段的所有实例都会被合并起来,就像这个方法
Message::MergeFrom
一样。(这个方法,对单标量的字段,后面相同字段的会覆盖前者;对于嵌入的message,则合并;对于重复的字段则连接起来) 以上这种规则有一个好处: 当你解析两个连接在一起编码的messages时,产生的结果就像你分别解析了两个独立的message一样。 举个例子:
MyMessage message;
message.ParseFromString(str1 + str2);
和下面的结果是等效的:
MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);
这个特点有时候很有用,因为它可以允许你在不知道另一个message的类型下进行合并结果。
Packed Repeated Fields
2.1的版本引入了Packed Repeated Fields
,
在proto3中,数字类型的repeated Fields
是默认包装的。这个功能和repeated fields有点类似,但是编码方式不同。如果一个打包的repeated 字段里面一个元素都没有,那么 编码中是不会出现这个字段的编码信息。否则,这个字段里面所有的元素都会被打包成一个key-value对,并且它的wire type是2(长度指定的)。每个元素都会按照正常的方式编码,除非它前面没有key。
例如,加入你有一个这样的message :
message Test4 {
repeated int32 d = 4 [packed=true];
}
现在,让我们看下这个Test4,假如给这个字段d
赋值3270,还有86942。那么,编码后的形式如下:
22 // key (field number 4, wire type 2)
06 // payload size (6 bytes)
03 // first element (varint 3)
8E 02 // second element (varint 270)
9E A7 05 // third element (varint 86942)
只有原始数字类型的重复字段(比如,varint,32-bit,或者64-bit)可以被声明为“packed”。
用在其他字段,编译 .proto
文件时会报错
注意,尽管通常并没有理由对packed repeated field
去编码超过一个key-value对,编码器必须要准好能接受多个键值对。这种情况下,payloads
信息将会被连接起来。每个键值对必须包含所有元素。
pb的解析器也必须有能力解析packed repeated field 就像它们没有被打包过一样,反之亦然。
这就允许你给已存在的字段添加 [packed=true]
,提供一种向前,或者向后兼容的方式。
字段顺序
在.proto
文件中,顺序的选择并不对message的序列化产生任何影响。
当一个message被序列化时,不保证字段被写入的顺序。
序列化的实现是一个细节,而且该细节未来有可能会变化。
因此,pb的解析器必须能够以任何方式的顺序解析字段。
启发
- 不要想着每次序列化出来的字节输出都是一样的。这一点对那些有其他pb message byte字段的message特别成立。
- 下面这些方法可能不成立
foo.SerializeAsString() == foo.SerializeAsString()
Hash(foo.SerializeAsString()) == Hash(foo.SerializeAsString())
CRC(foo.SerializeAsString()) == CRC(foo.SerializeAsString())
FingerPrint(foo.SerializeAsString()) == FingerPrint(foo.SerializeAsString())
- JavaScript 教程
- JavaScript 编辑工具
- JavaScript 与HTML
- JavaScript 与Java
- JavaScript 数据结构
- JavaScript 基本数据类型
- JavaScript 特殊数据类型
- JavaScript 运算符
- JavaScript typeof 运算符
- JavaScript 表达式
- JavaScript 类型转换
- JavaScript 基本语法
- JavaScript 注释
- Javascript 基本处理流程
- Javascript 选择结构
- Javascript if 语句
- Javascript if 语句的嵌套
- Javascript switch 语句
- Javascript 循环结构
- Javascript 循环结构实例
- Javascript 跳转语句
- Javascript 控制语句总结
- Javascript 函数介绍
- Javascript 函数的定义
- Javascript 函数调用
- Javascript 几种特殊的函数
- JavaScript 内置函数简介
- Javascript eval() 函数
- Javascript isFinite() 函数
- Javascript isNaN() 函数
- parseInt() 与 parseFloat()
- escape() 与 unescape()
- Javascript 字符串介绍
- Javascript length属性
- javascript 字符串函数
- Javascript 日期对象简介
- Javascript 日期对象用途
- Date 对象属性和方法
- Javascript 数组是什么
- Javascript 创建数组
- Javascript 数组赋值与取值
- Javascript 数组属性和方法
- 八种 Vue 组件间通讯方式合集
- Sharding-JDBC 实现分库分表
- fastjson——使用 aop 打印入参,报错:getOutputStream() has already been called for this response
- webpack从零搭建开发环境
- 博客——使用 Redis 实现博客编辑的自动保存草稿功能
- linux下安装zabbix服务器shell脚本-添加主机-邮件监控报警zabbix-自动化运维
- Nginx——开启 GZIP 压缩
- 谈谈Vue.use的原理
- Nginx——ubuntu安装Nginx并配置https
- Istio 中业务开发需要关注的二三事
- MongoDB——Ubuntu安装及配置带认证的副本集(亲测)
- 经验——SpringBoot 获取 resource 目录下的文件
- 聚类热图怎么按自己的意愿调整分支的顺序?
- H5|HTTP-FLV|WS-FLV|HLS|RTMP免费直播点播播放器如何自定义层叠DIV全屏后显示在视频上方?
- 什么是时间分片(Time Slicing)?