Protobuf学习
公司现在大部分协议都使用protobuf的格式,protobuf协议数据冗余数据小、序列化和反序列化速度快,但它序列化的数据在传输过程中是不可理解的(不像xml或jason那样抓到了包就可以直接看到数据内容)。
通过对protobuf的C++源码及python源码的一番简单阅读后,发现protobuf的序列化其实是个比较简单的过程。protobuf将数据分为6大类:
序列化后的数据,保存了每个字段的field_id、数据类型、数据内容(wiretype_length_delimited类型的数据还包含了一个数据内容的长度信息)这三个信息。field_id就是定义proto文件时,对每个字段定义的id数字,例如
message Test {
fixed32 fixed32_id = 16;
}
这个结构体中变量fixed32_id的field_id为2。一个字段序列化的格式如下:
浅绿色方块的内容只有当该字段的数据类型为wiretype_length_delimited时才有。可以看到序列化的数据分为三个数据块,field_id和数据类型放在同一个数据块中,数据长度在一个数据块中,数据内容放在一个数据块中。
数据类型只有6种,3个bit就足够表示了,所以第一个数据块中数据的内容为:data1 = (filed_id << 3) | (0x7 & 数据类型枚举值)。为了节约空间,protobuf并不是直接将data1写入到buffer中,而是采用了类似utf8的编码方式,每个byte只有后7个bit写数据,第一个bit用来表示下一个byte是否还属于这个数据块。例如:上述的Test结构体,只有一个字段,数据类型是wiretype_fiexed32,那么该字段的第一个数据块的内容则是 (16 << 3 ) | (0x7 & 5) = 0x85, 7个bit表示不了,所以至少需要两个byte, 第一个byte的值为 ((0x85 & 0x7f) | 0x80) = 0x85,第二个byte的值为 (0x85 >> 7) = 0x1。所以这里第一个数据块的数据就是两个字节:
由于uint32_id这个字段是wiretype_fiexed32类型,所以没有数据长度这个信息,数据内容是直接将fixed32_id这个字段的值按照32位整数的形式写入buffer,例如将结构体中fixed32_id赋值为1,则该结构体序列化后的数据为:
类似,如果数据类型是wiretype_fiexed64,则降数据按照64位整数的形式写入buffer。如果数据类型wiretype_varint,则数据内容的写入方式与第一个数据块类似,每个byte的第1个bit用来标志该数据是否结束,所以这里个人认为对于追求执行速度的程序,数据可以定义为fixed32、sfixed32、fixed64、sfixed64、float、double,这样序列化和反序列化省去”编码”过程,提高了执行速度,但对于存储和带宽是瓶颈的程序可以考虑将数据定义为int32、int64、uint32、uint64、sint32、sint64、bool、enum类型,类型对应表如下:
wiretype_length_delimited的数据比较特殊,它可以存放string、数组还可以存放一个自定义的message。当存放string类型的数据长度的数据块存放的是string的长度,数据内容数据块存放string的原始信息;当存放message的时候,数据长度的数据块存放的是message序列化为buffer后的长度,数据内容数据块存放message序列化为buffer后的内容。数据长度的写入方式跟wiretype_varint数据内容的写入方式一致。
参考:https://developers.google.com/protocol-buffers/docs/encoding