在计算机中,整数值通常有两种类型:32位以及64位,在Java中分别对应的是int和long,在C++中分别对应的是int32/uint32和int64/uint64。整型值按照二进制补码的形式存放。而且是定长的。也就是不论数值大小,类型一旦固定,占用的bit位数就是固定的。

那么protobuf协议如何序列化一个整数值?

protobuf使用Varints编码格式来编码整数,这个协议最大的特点是变长编码。怎么理解?也就是一个整数值编码后的字节数是不确定的,数值越小,占用的字节数也越少,比如127以内的数值,只需要一个字节,数值越大占用的字节数也越大。Varints编码需要征用每一个字节的最高位作为标记为,简称MSB,所以一个字节实际上只有7个bit在实际编码数据。那这个MSB位的作用是啥?是用来表示是否还需要读取下一个字节:如果是0,表示不需要,否则表示需要。这个思路有点类似utf-8编码。另外,Varints是小端序,需要将字节翻转。

看几个例子:

7 = 00000111 -> 7,可以在7个bit内表示,共占用一个1字节;

320 = 256 + 64 = 101000000(补码表示) -> 0000010 1000000(7bit分组) -> 00000010 11000000(从右向左标记MSB) -> 11000000 00000010(字节翻转) -> -64 2

所以,我们可以看到7使用了1字节,320会用了2字节。在数值较小时,还是很省空间的。

如果是要表示负数呢?

我们知道负数在补码中首位会是连续的1,也就是任意一个负数,开始都会是连续的1。这样的格式在Varints编码下会是怎样的?

举个例子:

以Java里的int类型为例,-64 = 11111111 11111111 11111111 11000000

虽然绝对值很小,但是补码整整占用了4个字节,在转为Varints编码后需要5个字节才能表示,而且不论负数的数值多大,都稳定占用5个字节。(这里说明一下,实际上在protobuff中,负数会稳定占用10个字节,也就是会按照long来编码,这里为了简化,暂且以5个字节来说明。至于为啥是10个字节,是因为如果将int32改为了int64,仍然可以兼容)。

【Protobuf(三)】Varints 与 Zigzag 编码_序列化

所以说,在表示负数时,Varints会退化为定长编码,不够高效。为了解决这个问题,引入了Zigzag编码,我们看一下:核心思路是将负数转化为其绝对值再进行Varints编码。怎么转化?0, -1, 1, -2对应为0, 1, 2, 3等等

【Protobuf(三)】Varints 与 Zigzag 编码_序列化_02

对应关系如上,类似一个拉链,所以才叫Zigzag。

那如何知道一个字节流使用了Zigzag编码?

protobuff里引入了sint32/sint64类型,只有声明为这两个类型才会对负数先使用Zigzag编码,否则直接使用Varints编码。

接下来看一下几种整数类型的区别(​​https://developers.google.com/protocol-buffers/docs/proto?csw=1#scalar​​):

有符号的:int32/int64、sint32/sint64

无符号:uint32/uint64

在Java里,由于没有无符号整数,所以uint和int是一样的。在C++中会有区别。那这几个类别怎么选择?

​https://stackoverflow.com/questions/765916/is-there-ever-a-good-time-to-use-int32-instead-of-sint32-in-google-protocol-buff​

如果确定没有负数,可以选择uint。在Java里,uint和int一样,所以选择两个都可以。

如果有负数,且出现频率较高,可以选择sint。否则选择int,毕竟sint还需要多一次编码。

另外,补码数值在转为字节流时,其实不关心是uint还是int,以及是int32还是int64,之所以需要区分这些类型,是因为需要在反序列时,知道转为对应语言下的哪一个类型。比如一个数值i=6,在int32和int64下序列化时一样的,同一个值需要反序列为int还是long,是根据pb里定义的类型来定的。

关于同一个值下的int32和int64序列化后的字节流一样的例子:

message TestMsg1 {
int32 a = 1;
int64 b = 2;
}

@Test
public void test4() {
MyProto.TestMsg1 msg = MyProto.TestMsg1.newBuilder()
.setA(4)
.setB(4L)
.build();
printHex(msg.toByteArray());
}

值为:8, 4, 16, 4。所以int的4和long的4,使用Varints序列化后的字节是一样的。int32和int64的却别在于反序列化时表示语言里的数据类型。

 

小结一下:

定长编码:一视同仁,所有数值都是相同空间,通过类型即可确定占用空间大小;变长编码:根据数值大小调整占用的空间,但是需要一个标识空间大小的信息,Varints是通过征用MSB位的方式;

Zigzag编码:Varints在表示负数时会退化为定长编码,引入了Zingzag及sintxxx,对绝对值Varins编码。如果负数较多,最好用sintxxx;

在Java里,intxxx和uintxxx一样,没有无符号整数。如果负数不多,用这俩就行,如果较多,最好用sintxxx;

 

参考链接:


 

​https://www.jianshu.com/p/73c9ed3a4877​

​https://ngtzeyang94.medium.com/go-with-examples-protobuf-encoding-mechanics-54ceff48ebaa​