首页 > 编程知识 正文

protobuf支持的数据类型,protobuffer序列化

时间:2023-05-04 10:44:13 阅读:153653 作者:445

ProtoBuffer Encoding原理分析准备工作Base 128 Varints编码proto的消息结构和数据类型其他数据类型带符号ZigZag编码非varints编码的数值型字符串型嵌入结构optional和repeatedpace

proto buffer编码原理分析

本文参考官方的编码文档

3359 developers.Google.com/protocol-buffers/docs/encoding

准备工作下载proto相关工具https://developers.Google.com/protocol-buffers

准备测试用的proto文件test.proto

包主; message Pb _ protobufencodingtest { optional int 32id=1; 选项字符串名称=2; 选项字符串地址=3; optional int64 number=4; }用官方提供的工具生成go代码

protoc.exe-- go _ out=.//test.proto然后打印一些简单信息的二进制数据和十六进制数据。

packagemainimport (' code.Google.com/p/goprotobuf/proto ' ' fmt ' ) func main ) { test :=Pb _ protobufencoding _:=rangeout(fmt.printf ),out ) I )/089601 ) fmt.println _ :=range out { fmt.printf (b ), out(I ) )/000010001001100000001 } } base 128 var ints编码主体首先从整数数据编码开始,proto的高压缩比是非常高的一切开始之前,整数数据的常用编码方式之一

Varints是一种使用可变长度字节序列化整数的方法,数值越低,字节数越少。 那么如何识别整数的边界呢? 答案是每个字节的第一位,如果连续字节表示同一整数,则除最后一个字节外,上一个字节的最高有效位将设置为1,每次遇到时最高有效位不为1的字节将与上一个连续最高位为1的字节一起表示整数。 也称为Base 128,因为由于最高有效位的存在,每字节实际存储数据的位只有7位。

如果为300,则普通二进制文件表示如下:

0000 0001 | 0010 1100是Base 128 Varints的二进制表示形式,第一个字节的最高有效位必须为1

10101100|00000010 base 128 var ints编码的二进制数据如何转换为普通二进制数据?

1 .删除每个字节的最高有效位。 最高有效位不是用于存储数据,而是用于记录每个整数的边界

10101100|000000100101100|00000102 .按相反顺序将字节转换为十进制

0101100|00000100000010|0101100 (反向) 100101100 )去掉最上面的0,可以看出这里已经是普通的二进制文件) 2563284=300 (转换为十进制)将上述Base 128 Varints格式的二进制转换为通常的二进制是解码的过程,根据最高位是否被设置为1,得到每个整数的字节数据,去除最高位

proto的消息结构和数据类型proto实际上是key-value结构的类型,编码时key和value相连写入二进制数据。 在解码时解析器必须能够跳过不知道的字段。 这样,在将同一Proto结构添加到新字段中时,可以保证旧协议的兼容性。 为了实现这一点,key实际上由两个值配置,每个字段的编号(field_number )是该字段的数据类型(wire_type )。

proto为每个数据类型定义wire_type,每个wire_type使用不同的编码方法。

WireTypeMeaningUsed For0Varin

tint32, int64, uint32, uint64, sint32, sint64, bool, enum164-bitfixed64, sfixed64, double2Length-delimitedstring, bytes, embedded messages, packed repeated fields3Start groupgroups (deprecated)4End groupgroups (deprecated)532-bitfixed32, sfixed32, float

​ 再回顾之前被 赋值 150 的 proto 对应的二进制数据,其中 08 就是 key 值,通过 (1 << 3 | 0 )计算得到

key = (field_number << 3 | wire_type )

​ 那么如何将 08 解析成 field_number 和 wire_type呢,其二进制的后三位就是 wire_type, 剩下的右移三位就是 field_number。通过对 key 的解析, 我们可以得到的接下来的字节所表示的数据类型 ,如果是 Varint 就按照上述 Base 128 Varint 的方式解析。

其他的数据类型 有符号数的 ZigZag 编码

​ 可以看到 wire_type 为 0 的都使用 Varint 方式编码,其中还包括 signed int 类型。对于有符号的整数要如何表示呢? 虽然都是整数,但是当有负数存在的时候,正数和负数的表示完全不一样。如果用 int32/int64 表示负数,将使用到长达 10个字节。而用 signed int 类型表示负数时,使用的是更高效的 ZigZag 编码。

​ ZigZag 会将有符号的负数转换成无符号的正数。比如 -1 将被编码成 1 , 1 编码成 2….

sint32 的编码计算(n << 1) ^ (n >> 31)sint64 的编码计算(n << 1) ^ (n >> 63) 非 varints 编码的数值类型

​ 比如 double 和fixed64 , 都是 wire_type 1 格式的, 固定使用 64 位来存储数据,同样的,float 和 fixed32 固定使用 32 bits 来存储数据。

字符串类型

​ 字符串类型的采用变长编码,这一点跟很多协议的编码类似,第一个字节表示字符串长度,剩下的是 UTF-8 编码,比如 “testing” 的编码为:

12 07 74 65 73 74 69 6e 67

0x12 是key , 解析得到 field_number和 wire_type , 根据 wire_type 得知接下来是字符串类型。然后 0x07 表示字符串长度为 7个字节,那么接下来的 7 个字节就是字符串的内容。

嵌入结构

将上面的Proto结构再嵌套一层

message PB_EmbeddedTest{ optional PB_ProtoBufEncodingTest t = 3;}

打印下嵌套后的proto的字节流

test2 := &PB_EmbeddedTest{ T: test,}out2, _ := proto.Marshal(test2)for i, _ := range out2 { fmt.Printf("%02X ", out2[i]) //1A 03 08 96 01 }

可以看到,后3个字节跟之前的是一样的,第二个字节 0x03 表示后面数据的长度,这一点跟字符串一样。第一个字节 1A 是 field_nubmer 和 wire_type 编码的结果。

optional 和 repeated

​ 处理完了所有的数据类型,还有一个问题就是数组如何编解码。在 proto 中数组用 repeated 表示。使用reapted 表示的数据,共用的是同一个 field_number 和 wire_type。在存储的时候,数组数据实际上并不一定是连续存储的,而可能被其他类型的数据分割开。

​ 对于 proto2 中 optional 类型和 proto3 中所有 非repeated 的数据,没有值的情况下是不会被编码的。

​ 正常的,如果不是 repeated 类型的字段,编码后的数据是不可能出现多种类型相同的数据的,也就是 key 不会相同。一般各个语言的 API 也不会出现这种情况,但是对于给定的符合条件的二进制流,如果出现了相同key 的 非 repeated 类型的数据,解码器会对这种情况做做一些处理,如果是 int / string 类型的,会取最后一次的赋值结果,对于重复的嵌套结构,则是做合并操作。 合并操作不同的语言生成的 API 不一样,以go 的为例,是这样:

// API Generatedfunc (dst *PB_ProtoBufEncodingTest) XXX_Merge(src proto.Message) { xxx_messageInfo_PB_ProtoBufEncodingTest.Merge(dst, src)}//Exampletest := &PB_ProtoBufEncodingTest{ Id: proto.Int32(150), Name: proto.String("hello"),}// 等效于 ====================>test := &PB_ProtoBufEncodingTest{ Id: proto.Int32(150),}test2 := &PB_ProtoBufEncodingTest{ Name: proto.String("hello"),}test1.XXX_Merge(t2) Packed Repeated Fields

​ 对于repeated类型的字段可以进一步优化,proto2 中需要使用 [packed=true]; proto3 是默认会优化的。一个声明了 packed 的 repeated 字段会被压缩到一个 key-value 对,其 wire_type 为 2。其优化的空间实际上是数组中多余的 key 占用的空间。

​ 直接看两种编码方式会更直观,对于一个声明了 packed=true 的 int 数组 d

message Test4 { repeated int32 d = 4 [packed=true];}

​ 其 packed 前后的区别如下:

//packed 之后 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)//packed 之前20 // key (field number 4, wire type 0)01 // payload size (1 bytes)03 // first element (varint 3)================================================20 // key (field number 4, wire type 0)02 // payload size (2 bytes)8E 02 // second element (varint 270)================================================20 // key (field number 4, wire type 0)03 // payload size (3 bytes)9E A7 05 // third element (varint 86942)

​ 当然,packed 功能很明显只能适用于数值类型,比如 varint , 32-bit ,64-bit , 因为数组的所有元素是连续存放在一起的,只有数值类型,才能区分出每个元素明显的边界,比如 varint 使用最高位来标记边界,而 32-bit 和 64-bit 是固定大小的存储空间。 字符串无法使用 packed,是因为本身就没有用于区分边界的字段。

​ 协议的解析器是可以支持将标记为 packed 的字段当作没有标记一样处理和解析的。换句话说,packed 标记是前后兼容的,任何时候去掉这个标记,对已产生的数据的解析都是不影响的。

filed_number 和 field order

proto 里是根据的 field_number 来决定一个字段的解码方式的,因此,字段定义的顺序不重要。

proto 压缩方面的优势

​ 与 JSON 相比

proto 对数值类型的数据压缩达到了极致,json 中所有的整型都是 long 型,proto 使用的 varints 和 zigzag 等编码方式将数值类型的占用空间压到最低。特别是在数值类型的数组上,使用packed 将进一步压缩key占用。proto 对字符串基本没有压缩,也就是传统 len + value 的格式,这一点跟 json 差不多proto 中使用一个字节的key 即可处理边界问题,而 json 有大量的 { } ," " 等分界符。proto 对于optional 的字段可以选择不进行序列化 更多

源码解析:

https://halfrost.com/protobuf_decode/

性能压测实验:

https://github.com/eishay/jvm-serializers/wikis

版权声明:该文观点仅代表作者本人。处理文章:请发送邮件至 三1五14八八95#扣扣.com 举报,一经查实,本站将立刻删除。