一条消息数据,用protobuf序列化后的大小是json的1/10,是xml格式的1/20,但是性能却是它们的5~100倍,那么这又是怎么做到的呢?

Value部分

Protobuf的IDL(Interface Description Language)格式中,传输数据的两端都需要定义该消息结构,并保存在.proto文件中,这样就不需要在消息数据中定义结构信息数据通信时,只需要传输变量名的值(value)部分,减少了传输数据

message Person{
    //数据类型-变量名-编号
    int32 id = 1;
    string name = 2;
    int32 age = 3;
}

Tag分隔符

那么,怎么界定value是否传输完了呢,这就引入了Tag,比如:Tag1TagxiaomingTag20,通过Tag进行分割。同时,通过Tag的位运算方算法(如下图)也可以识别某个变量不传输的情况,比如:Tag1Tag20,name没有被传输,Tag中的fieldNumber左移三位后正好和wireType错开,或运算后就可以通过一个int变量同时表示wireType和fieldNumber两个信息。

//fieldNumber是编号,wireType如下图所示,一共有6种不同的类型(000-101)
static int makeTag(final int fieldNumber, final int wireType) {
  return (fieldNumber << 3) | wireType;
}

wireType

Varint编码

Tag分隔符为int类型,存储4个字节,但127这个数字,只有1个字节为有效信息,如果传输中的内容出现相同的字节,通过Varint编码保证不会发生解析错误,同时Varint可以以不同的长度来存储整数,将数据进一步的进行了压缩。

比如267,它的Varint编码表示为:10001011 00000010,第一个字节最高是1,表示下一个字节也是其想表述的数据的组成部分;反之,0则表示下一个字节与当前字节没有关系。

这样的话,其实上面16位里,只有14位(0001011(低) 0000010(高))是有实际数据意义的,从左到右先放高位,那么就是0000010 0001011,连一起就是00000100001011,正好就是前面我们的例子267的二进制表示

Zigzag编码

但是这里面也有一个问题,在计算机当中的负数是用补码表示的,对于-1,它的二进制表示方式为:11111111 11111111 11111111 11111111(4字节32位)
显然无法用1个字节来表示了,但-1确实是一个比较简单的数,这个时候就可以使用zigzag算法来对负数进行进一步的压缩,最终我们可以使用2个字节来表示-1。

Zigzag编码规则如下:

  • 如果数据是负数,那么套用2*|x|-1来编码表示
  • 如果数据是正数,那么套用2*|x| 来编码表示

值得注意的是Zigzag也同样用Varint来编码。

Length字符串长度信息

在传输字符串的时候,我们加入一个字符串长度信息,组成Tag-Length-Value,俗称TLV,因为传输字符串的时候Varint的编码效率低,通过Length来提高他的编码效率,值得注意的是Length也同样用Varint来编码。

数据的解析流程

如果我们要传输一个数字,拿到第一个Tag,解析出来他的fieldNumber和wireType,因为采用varint来编码,高位为1说明下个字节还是该数字,如果为0说明下字节为Tag了。解析完以后,假设我们下一个要传输的是一个字符串,那么拿到Tag后通过wireType就知道下一个要传字符串了,那么下一个字节开始解析Length,Length同样是Varint编码,高位为1说明下个字节还是Length,为0说明这个字节后就解析完了,我们就拿到了Value的长度,接下来按照长度读取完字符串后,下一个字符就是Tag了,以此类推直到解析完包内所有的数据。

参考资料

b站-Protobuf为什么这么快?

csdn-protobuf 为什么快

csdn-为什么protobuf这么快

高效的数据压缩编码方式 Protobuf

小而巧的数字压缩算法:zigzag