Protobuf为什么快

Protobuf接收/发送方都需要维护一个IDL,其中格式如下

例:Protobuf的IDL

message Person {
    required int32 id = 1;
    required string name = 2;
    required string hobby = 3;
    required string introduction = 4;
}

注意:Protobuf在传输时,只需要传输value,因此速度就可以做到比其他序列化更快了

如下面这段Protobuf字节流(|为了做区分打上的, 实际上是另一种表现形式)

1|zmp|play tennis|hello world

接收端接收到之后,就可以按照事先定义好的IDL解析成key-value

id=1
name=zmp
hobby=play tennis
introduction=hello world

问题1:如何划分数据

在Protobuf中, 会生成一个Tag字段,用于区分数据

传送格式:

Tag1TagzmpTagplay tennisTaghello world

其中的Tag生成:

static int makeTag(final int fieldNumber, final int wireType) {
    return (filedNumber << 3) | wireType;
}

其中fieldNumber代表的是IDL中的编号, 例如上面id的编号就为1

wireType的数字可以根据下表所示

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

可以看到wireType仅需要3位即可完成(从000 ~ 101)

那么就知道了生成的Tag中, 低3位是wireType,然后后面的就是自己定义的fieldNumber

问题2:如果传输过程中发现了和Tag格式一致的字节怎么办

因为Tag生成后是一个1字节的int类型,那么不可避免我们的业务数据中有一个字节会巧合与Tag的格式一样

传输数字时

而Protobuf在传输信息时,对于数字都会使用varint编码和zigzag编码,关于这两种编码方式这里不展开了,大家可以去搜索一下有很多相关原理介绍

例如-1的编码过程如下所示

-1的补码表示形式为
11111111 11111111 11111111 11111111
进行zigzag之后
00000000 00000000 00000000 00000001
再进行varint编码
00000001

可见省去了3/4的体积

例如一开始要传输一个数字,我们拿到第一个Tag,解析出它的fieldNumberwireType,在varint编码的作用下,高位为1表示下一个字节还是该数字,如果为0则表示下一个字节是Tag了

传输字符串时

对于传输字符串的情况,使用varint编码的效率就比较低了,这时候可以添加一个长度相关信息,使其整个格式为Tag-Length-Value形式,Tag为分隔符,那么Length同样也可以使用Varint编码方式

如果一开始传输的是一个字符串,拿到Tag后,就知道接下来是一个字符串,那么下一个字节就开始解析Length,Length同样适用varint编码,遇到0后辨识该Length解析完成,那么就知道接下来Value的长度,按照长度取完字符串之后,那么下一个就又回到了Tag

以此类推,Protobuf永远知道哪一个字节是Tag,这样就可以很好的进行区分

Last modification:June 27th, 2022 at 08:17 pm