protobuf序列化原理

本文详细介绍了protobuf的序列化原理,包括varints编码、Message结构、更多数据类型的编码(如Signed Integers、Strings、Embedded Messages)以及Optional和Repeated Elements的处理。重点阐述了varints如何高效地存储整数,以及在处理有符号整数和字符串时的特殊编码方式。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

varints

在了解protobuf编码原理之前,首先要了解的是varints
varints是一种用一个或多个字节来序列化整数的方法。整数的值越小,占用的字节数就越少。

varints的每个字节的第一个bit最高有效位(msb)是用来指示下一个字节是不是还是用来表示这个整数的。可以将它看成一个单链表的next指针,为1则指向下一个节点,为0则指向null。而varints较低位的7个bit以二进制补码、低字节序(小端)的形式存储整数。

例如:
有以下varints表示的整数:
1010 1100 0000 0010

要解码得到原来的数值,首先要去掉每个字节的msb(msb只是用来告诉这个整数是不是已经到结尾了,计算数值时并没有用),得到了一下没有msb的形式:
010 1100 000 0010

然后,由于varints是低字节序的,因此需要先对字节进行一个反转(reverse)操作,得到:
000 0010 010 1100
→ 100101100
→ 256 + 32 + 8 + 4 = 300


Message

protobuf message是一系列的键值对,message的二进制形式使用字段的tag数值作为key,而其的字段名以及实际的数据类型则只能在解码端根据message的类型定义来决定。

编码message时,key和value被连接成字节流。当解码message的时候,解释器要有能力跳过那些他不能识别的字段,这样子,才能够添加新的字段进message而且不会影响不能识别这些新字段的老程序。

为此,每个字节流上的每个key实际上是由两个值组成的——一个是字段的tag number,另一个是
该字段的 wire type,这是用来提供足够的信息来明确该字段的值得长度的。

下面是可选的wire type

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages, packed repeated fields
3Start groupgroups(已废弃)
4End groupgroups(已废弃)
532-bitfixed32, sfixed32, float

字节流上的每个key都是varint。key的值是(字段tag number<<3 | wire_type),即最后3个bit用来存储该字段的wire type

举个例子,有以下message的定义:
message Test1 {
required int32 a = 1;
}

在某个应用中,产生了一个Test1 message,a的值为150。将这条message序列化成二进制形式后,得到3个字节:
08 96 01

则二进制中key为08,即:
0000 1000

①从最后3个bit能判断出该字段的wire type为0(Varint)
②然后将该key右移3个bit,得到0000 0001
得知这个字段的tag number为1。

根据varint来解析后面两个字节存储的值:

96 01 = 1001 0110 0000 0001
        → 000 0001 ++ 001 0110 (去掉msb并对字节进行反转)
        → 10010110
        → 150

更多数据类型

Signed Integers

有符号整数类型(sint32sint64)和“标准”整数类型(int32int64)都是编码成varints,但是,当他们的值是一个负数时,会有很重要的不同。

当你使用int32int64存储负数时,总会产生一个长度为10个字节的varint——它被看做是一个十分大的无符号整型。

而如果使用其中一种有符号整型(sint32sing64),则会使用ZigZag编码产生varint,效率比int32int64高。

ZigZag 编码使用”zig-zags”在正负数间回退前进的方法,使-1编码成1,1编码成2,-2编码成3,如此类推:

Signed OriginalEncoded As
00
-11
12
-23
21474836474294967294
-21474836484294967295

换言之,每个值n都会根据以下公式进行编码:

对于sint32:
(n << 1) ^ (n >> 31)

对于sint64:
(n << 1) ^ (n >> 63)

注意,这里的第二个移位——n >> 31 部分,是算术移位,也就是说,如果n是正数,则移位的结果是所有位都是0,如果n是负数,则所有位都是1。

分析sint32sint64时,其值会被解码成原始的有符号的形式。

举个例子:

a = -10xffffffff(计算机用补码表示负数)
a << 10xfffffffe  →   -2
a >> 310xffffffff  →   -1

(a << 1) ^ (a >> 31)    →   0xfffffffe ^ 0xffffffff0x000000011

Strings

wire type 2(length-delimited)表示一个这个字段的值由一个varint n和其后跟随的n个字节的数据组成。

message Test2 {
    required string b = 2;
}

设b的值为”testing”,序列化得到:

12 07 73 74 69 6e 67

红色的字节是UTF8编码的“testing”,key是0x12 → tag = 2, type = 2,。长度有一个varint表示,即0x07→7,所以后面的7个字节就是字符串“testing”。

Embbed Messages

在序列化时,对于内嵌message,实际上和对string的处理方式是一样的。

message Test3 {
    required Test1 c = 3;
}

设Test1 c的a字段为150,序列化将得到:

1a 03 08 96 01

可以看到,最后3个字节实际上和我们之前举得例子(08 96 01)是一样的。

Optional And Repeated Elements

正常来说,一条序列化的message,不会多次出现同一个non-repeated字段。但是,分析器对这种情况设置了处理的方法。

对数值型和字符串类型,如果同一个字段多次出现,则分析器以最后一次出现的值为最终结果。

对于内嵌message,分析器将同一个字段多个值合并,就像使用Message::MergeFrom方法一样。

这些规则使得分析两条连接在一起的序列化的message实际等于分别解析这两条message再对他们执行merge后得到最终结果。即:

MyMessage message;
message.ParseFromString(str1 + str2);

和这样是相同的:

MyMessage message, message2;
message.ParseFromString(str1);
message2.ParseFromString(str2);
message.MergeFrom(message2);

Packed Repeated Fields

对于proto2 message,如果一个repeated字段没有设置[packed=true],那么序列化将会有0个或多个键值对,每个键中都包含tag,就像上面提到的其他类型字段一样。这些repeated的键值对不一定是连续的,可能会和其他字段交错出现。但每个repeated的键值对出现的位置和他们实际应该的顺序是保持一致的。

从2.1.0版本起,protobuf引入了一种packed repeated fields,声明方式与repeated fields一样,只是需要设置[packed=true]选项。

在proto3中,repeated fields默认就是packed repeated fields。

packed repeated field和一般的repeated field不同,对于多个值,序列化后,将只会产生一个键值对,对于每个值,都会根据其类型进行序列化(但是没有tag),然后将所有的值拼接后以wire type 2(length-delimited)的方式打包成一个值。

例如,

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

生成一个Test4,d为{3, 270, 86942}。将该message序列化将得到:

22          // tag number 4, wire type 2
06          // 长度为6个字节
03          // 第一个元素为varint 3
8E 02       // 第二个元素为varint 270
9E A7 05    // 第3个元素为varint 86942

只有原始数值型(varint, 32-bit, 64-bit)的repeated字段可以被声明为“packed”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值