【技术类】PB二进制序列化

自从使用protobuf作序列化工具之后,每次面试都问我,为什么用?
很迅速的回答了体积小,解析快。
为什么小,采用了varint的压缩方式,那你讲一下这个压缩方式,然后emmm…
那为什么解析快,有没有跟其他做过对比,又是emmm…

test.proto
syntax = "proto2";

message Test
{
	required int32 first = 1;
	optional int32 second = 2;
	optional string third = 3;
}
这里使用C++语言,编译到当前目录命令: protoc test.proto --cpp_out=./
产生两个文件:test.pb.h	test.pb.cc(动态链接库)
#include <iostream>
#include "test.pb.h"
using namespace std;

int main()
{
	Test st;
	st.set_first(10);
	st.set_second(20);
	st.set_third("hello");
	
	char buffer[64] = {0};
	if(!st.SerializeToArray(buffer,st.ByteSize()))
		cout<<"Serlialize Error"<<endl;
	return 0;
}
//由于序列化的结果中含有0,所以采用gdb调试打印看,结果如下:
$1 = "\b\n\020\024\032\005hello", '\000' <repeats 52 times>
下面的文献内容,摘自大佬的博客,自己只是用来学习,附上地址,如有侵犯,联系本人,会尽快处理。
https://blog.csdn.net/chengzi_comm/article/details/53199278

protobuf的message中每个字段的格式为: 修饰符 字段类型 字段名 = 域号;
在序列化时,protobuf按照TLV的格式序列化每一个字段,T即Tag,也叫Key;L是Value的长度,如果一个字段是整形,这个L部分会省略;V是该字段对应的值value。

序列化后的Value是按原样保存到字符串或者文件中,Key按照一定的转换条件保存起来,序列化后的结果就是 KeyValueKeyValue。

Key的序列化格式是按照message中字段后面的域号与字段类型来转换。转换公式如下:
(field_number << 3) | wire_type,wire_type与字段的类型有关(不同类型采用的压缩方式不一)

 

Message Structure

PB 的 Message 是以一系列有序的 KV 构成的,key 就是字段的 number+wireType,这个在 IDL 文件定义的时候就有的;value 就是对应的值。这里的 Key 结构大致是这样的:

(field_number << 3) | wire_type

field_number 就是字段序号,本身也是Varints 类型的,wire_type 表示字段类型,占用3个 bit。关于 wire type,这里有个对应关系表:

 

比如:int32,int64等采用varint(wire_type=0),string就采用Length-delimi(wire_type=2)
这里只列举这两个,其他的type类型可以去官网查看。

protobuf规定:
1.如果域号在[1,15]范围内,会使用一个字节表示Key;
2.如果域号大于等于16,会使用两个字节表示Key;
3.Key编码过后,该字节的第一个比特位表示之后的一个字节是否与当前这个字节有关:
	a.如果第一个比特位为1,表示有关,即连续两个字节都是Key的编码;
	b.如果第一个比特位为0,表示Key的编码只有当前一个字节,后面的字节是Length或者Value;
计算first
key = (1 << 3) | 0 = 8, 8对应的ASCLL就是‘\b’,因为是int型所以不需要指定长度
那么‘\n’就是value,10喽。

second
key = (2 << 3) | 0 = 16, 16的八进制码是20
24就是value(20)的八进制码了。

third
key = (3 << 3) | 2 = 26,26的八进制码是32,后面的5就是长度,以及后面的内容了

到这里我们只是把数据对应上了,体积小表现在哪?

我们可以清晰的看到10和20两个4字节的数被编码成了一个字节的八进制数。怎么做的。
例: 10的二进制码 = 00000000 00000000 00000000 00001010
	1.从最后字节取7位,如果当前字节后面没字节了,那么就在最高位补1,否则补0。
	2.讲得到的字节拼接起来得到00000000 00000000 00000000 10001010
	3.如果字节表示的数为0,那么舍弃该字节,此时得到的就是10001010
做反序列化工作时:第一位是1,那么剩下7位都是数据,则为0001010 = 10。

如果数据比较大时,那么编码可能会占用两个字节,比如300:10101100 00000010 
解析工作:高位为0,代表这是最后一个字节,1代表还有数据。去掉最高位拼接得到 0000010 0101100

我的求证:

message UserLoginREQ { optional int64 uid = 1; optional string key = 2; optional string ver = 3; optional string ip = 4; optional int32 login_type = 6[default = 0]; //0.登录 1.断线重连 }

 

转成的 PB序列化,

pb长度:0x58;

有缺省的pb字段。

szPbData 58 08 2F 12 20 62 64 64 30 37 32 37 30 62 64 63 63 64 64 66 61 33 61 66 38 34 66 31 30 65 37 38 65 39 63 66 34 1A 07 34 2E 31 39 2E 32 39 22 09 31 32 37 2E 30 2E 30 2E 31 30 00

 

 

 

发送的内容:

uid: 47 key: "34f3629dc31aa03782addddef6cbc86d" ver: "4.19.29" ip: "127.0.0.1" login_type: 0

 

 

完整的PB协议:

message UserLoginREQ
{
optional int64 uid = 1;
optional string key = 2;
optional string ver = 3;
optional string ip = 4;
optional int32 client_type = 5 [default = 1];//客户端类型 1 cocos 2 h5
optional int32 login_type = 6[default = 0]; //0.登录 1.断线重连
optional string system = 7; //用户系统信息ios or andirod or windows
optional bool is_multipc = 8[default=false]; // 多进程pc标志
}



posted @ 2020-09-22 16:15  wulongming88  阅读(1099)  评论(0编辑  收藏  举报