1.protobuf编码相比于json更小的原理
通过之前的文章可以了解到,对于以下一个json对象
{
"name":"john",
“age":5
}
编码成protobuf时(这里我为了可读性更好,所以做了一个小转换),就会变成以下形式
1john25
意思是,第一个字段是john,第二个字段是数字5
而如果采用json形式
{"name":"john",“age":5}
我们会发现,json为了实现自解释性,从而需要将字段名包含在编码中,且为了格式需要,还需要包含很多标点符号
而protobuf牺牲了自解释性和可读性,从而使得编码的结果比json小了很多,因此采用这种方式就可以随意命名字段的名称了,反正编码后都是固定的形式
那么字段名等信息又如何获得呢?
这些信息是通过protobuf生成的具体语言的解析代码实现的,也就是说,如果A系统将某些信息编码成了protobuf的形式,并且希望传输给B系统,那么B系统也必须使用相应的解析代码进行解析,而2个系统的生成解析代码的依据必须是同一份proto文件。
所以归纳来说,protobuf是通过将数据的一部分信息,直接落地在系统上,从而减少了传输过程所需要的数据量,即以系统存储空间换数据传输空间
2.一个小戏法
从前文可以知道,protobuf的数据在传输过程中其实是缺失数据的字段名的,那么利用这一点,我们可以使得A系统发送的某个对象,在B系统中被解析成另外一个对象,即不同模型间的相互转换
我们定义如下2个模型
message Person {
string name = 1;
int32 id = 2;
string email = 3;
repeated string contacts = 4;
}
message Computer {
string type = 1;
int32 cpu = 2;
string merchant = 3;
repeated string disks = 4;
}
在demo的ProtobufTest.ModelTransformationTest()中,我们可以看到2个不同的模型之间是能互相转换的
当然这种转换的限制非常大,2个模型的字段名可以完全不同,但是相应的字段类型和序号则必须一致,否则将会抛出异常
因此该trick的实用性并非很高,但在服务端的数据交互中可以起到一定作用
例如,你需要从A系统请求数据,然后将其转换成自己系统中定义的模型,在使用json格式的时候,我们要么完全严格地按照A系统定义的字段名创建模型,或者是写一些非常冗余且低效的代码,将数据中的字段一一赋值给自己定义的模型中相应的字段,那么此时这个trick就可以起到一定的作用,它能让你任性地定义自己喜欢的字段名!是不是感觉有点棒!
3.关于序号字节对于编码大小的影响
我们现在回顾一下protobuf的编码原理,其中有提到,序号字节的编码最低3位表示字段类型,最高位是用来判断是否到达最后一个字节,中间4位才用来表示序号,因此单个字节表示序号的时候,一个模型最多定义15个字段
当我们定义超过15个字段时,且这些字段都被赋值时,编码后的序号字段将需要至少2个字节表示
例如之前我们给Person对象的序号为23的large字段同样赋值150,得到的结果是
-72 1 -106 1
10111000 00000001 10010110 00000001
序号是
10111000 00000001
那么就需要思考一个问题,如果我们某个对象有超过15个字段,那么超过的部分在编码时就会多出一个字段,当模型字段特别多的时候,这些多出的字节也将是不小的一部分
为了尽量避免这个问题,可以考虑采用子对象的形式
某个对象有30个字段,在编码时会多出15个字节的额外量,那么我们可以定义2个子对象,并将30个字段分别定义在2个子对象中,每个子对象15个字段,那么在编码时就可以节省部分字节
具体实例:
我们定义一个模型,其中包含30个字段,另一个模型包含2个15个字段的子对象
比较结果为
115
126
为何只少了11个字节呢?从前文子对象的编码过程中会发现,每一个子对象也需要有一个字节去标记其类型,还有一个字节标识其长度,因此2个子对象共需要额外4个字节,那么原先节省了15个字节,所以总计15-4=11个字节
当然这种形式会使得字段的读取略微麻烦一些,并且整体的节省比例会随着实际每个字段数据量的增加而缩小,因此比较适合于对传输过程有极限要求的场景
在demo的ProtobufTest.compareNestedAndNotNestedModel()和ProtobufTest.compareFieldNest()中我展示了这种对比