Protobuf(Protocol Buffer)是Google出品的一种轻量且高效的结构化数据存储格式,性能比Json、XML更强,被普遍应用于数据传输中。然Protobuf中的数据类型众多,什么场景应用什么数据类型最合理、最省空间,成为了每一个使用者该考虑的问题。为了能更充分的理解和使用Protobuf,本文将聚焦Protobuf的基本数据类型,分析其不一样数据类型的使用场景和注意事项。html
注意:在阅读本文以前最好对Protobuf的语法和序列化原理有必定的了解。
推荐文献:
【1】序列化:这是一份颇有诚意的 Protocol Buffer 语法详解 https://blog.csdn.net/carson_...
【2】Protocol Buffer 序列化原理大揭秘 - 为何Protocol Buffer性能这么好? https://blog.csdn.net/carson_...
【3】经过一个完整例子完全学会protobuf序列化原理 https://cloud.tencent.com/dev...
整型范围java
无符号整型范围segmentfault
浮点数范围数组
浮点数的存储方式详见: https://cloud.tencent.com/dev...
Protobuf的基本数据类型与JAVA的数据类型映射关系表以下:性能
映射表来源于Protobuf官网, https://developers.google.com...
注意到JAVA中没有区分无符整型和有符整型,Protobuf的int和uint统一映射到JAVA的int/long数据类型。测试
Protobuf数据类型的序列化方法粗略能够分为两种,一种是可变长编码(如Varint),Protobuf会合理分配空间存储数据,在保证不损失精度的状况下用尽可能小的空间节省内存(好比整数1,若数据定义的类型为int32,原本须要8个字节表达的,Protobuf只须要一个字节表达。注意,Protobuf只能节省到字节的单位(8个字节省到1个字节),而不能节省到位的单位(1个字节内还能够进一步省二进制位),这个后续开专题再聊);另外一种是固定长度编码(如64-bit、32-bit),数据定义的什么类型就占用多大空间,不论是否有浪费;其实,还有一种比较特别的方法(Length-delimited),这种方法主要针对相似于数组的数据,添加了一个字段记录数组的长度,而后将数组内容顺序组合,详细原理不在赘述,可见前文推荐的文献。ui
为验证Protobuf各数据类型的序列化效果,遂设计如下数据实验。google
一、首先,自定义了proto文件,使其中包含基本数据类型,并将proto生成java类(如何基于IDEA一站式编辑及编译proto文件,详见上一篇专题文章https://segmentfault.com/a/11...)。
proto文件内容以下:编码
// Google Protocol Buffers Version 3. syntax = "proto3"; option java_package = "learnProto.selfTest"; option java_outer_classname = "MyTest"; message Data{ uint32 uint32 = 1; uint64 uint64 = 2; int32 int32 = 3; int64 int64 = 4; sint32 sint32 = 5; sint64 sint64 = 6; fixed32 fixed32 = 7; fixed64 fixed64 = 8; bool bool=9; string str = 10; float float=11; double double=12; }
二、其次,分别对每一个数据类进行赋不一样的值并序列化,观察不一样数据序列化后占用的字节数。
三、最后,总结概括,造成使用建议。spa
测试代码以下:
public class demoTest { public void convertUint32(int value) { //1.经过build建立消息构造器 MyTest.Data.Builder dataBuilder = MyTest.Data.newBuilder(); //2.设置字段值 dataBuilder.setUint32(value); //3.经过消息构造器构造消息对象 MyTest.Data data = dataBuilder.build(); //4.序列化 byte[] bytes = data.toByteArray(); System.out.println(value+"序列化后的数据:" + Arrays.toString(bytes)+",字节个数:"+bytes.length); } ... // 此处省略其余数据类型的convert方法,如convertInt32与convertUint32方法代码相似,只须要修改set方法便可。 @Test public void test32(){ System.out.println("=================uint32================"); convertUint32(1); convertUint32(1000); convertUint32(Integer.MAX_VALUE); convertUint32(-1); convertUint32(-1000); convertUint32(Integer.MIN_VALUE); System.out.println("=================int32================"); convertInt32(1); convertInt32(1000); convertInt32(2147483647); convertInt32(-1); convertInt32(-1000); convertInt32(-2147483648); System.out.println("=================sint32================"); convertSint32(1); convertSint32(1000); convertSint32(2147483647); convertSint32(-1); convertSint32(-1000); convertSint32(-2147483648); System.out.println("=================fix32================"); convertFixed32(1); convertFixed32(1000); convertFixed32(2147483647); convertFixed32(-1); convertFixed32(-1000); convertFixed32(-2147483648); }
运行结果以下:
=================uint32================ 1序列化后的数据:[8, 1],字节个数:2 1000序列化后的数据:[8, -24, 7],字节个数:3 2147483647序列化后的数据:[8, -1, -1, -1, -1, 7],字节个数:6 -1序列化后的数据:[8, -1, -1, -1, -1, 15],字节个数:6 -1000序列化后的数据:[8, -104, -8, -1, -1, 15],字节个数:6 -2147483648序列化后的数据:[8, -128, -128, -128, -128, 8],字节个数:6 =================int32================ 1序列化后的数据:[24, 1],字节个数:2 1000序列化后的数据:[24, -24, 7],字节个数:3 2147483647序列化后的数据:[24, -1, -1, -1, -1, 7],字节个数:6 -1序列化后的数据:[24, -1, -1, -1, -1, -1, -1, -1, -1, -1, 1],字节个数:11 -1000序列化后的数据:[24, -104, -8, -1, -1, -1, -1, -1, -1, -1, 1],字节个数:11 -2147483648序列化后的数据:[24, -128, -128, -128, -128, -8, -1, -1, -1, -1, 1],字节个数:11 =================sint32================ 1序列化后的数据:[40, 2],字节个数:2 1000序列化后的数据:[40, -48, 15],字节个数:3 2147483647序列化后的数据:[40, -2, -1, -1, -1, 15],字节个数:6 -1序列化后的数据:[40, 1],字节个数:2 -1000序列化后的数据:[40, -49, 15],字节个数:3 -2147483648序列化后的数据:[40, -1, -1, -1, -1, 15],字节个数:6 =================fix32================ 1序列化后的数据:[61, 1, 0, 0, 0],字节个数:5 1000序列化后的数据:[61, -24, 3, 0, 0],字节个数:5 2147483647序列化后的数据:[61, -1, -1, -1, 127],字节个数:5 -1序列化后的数据:[61, -1, -1, -1, -1],字节个数:5 -1000序列化后的数据:[61, 24, -4, -1, -1],字节个数:5 -2147483648序列化后的数据:[61, 0, 0, 0, -128],字节个数:5
【小结】
一、 uint32类型:数值范围等价于int32的范围(能够存负数,由于proto没有对负数进行判断及限制)。正数最多占用5个字节,负数必占用5个字节。(第一个字节存储的是数据类型和字段在proto中的编号,即原理篇里讲的tag。之因此32位的数据最多要用5个字节来存储,是由于每一个字节的最高位须要记录该数据是否衍生到下个字节(为实现可变长存储),1表示衍生,0表示不衍生。因此每一个字节的实际存储数据的位数为7,则4*7<32,所以须要5个字节)
二、 int32类型:存正数时最多须要5个字节,存负数时一定须要10个字节。(由于存负数时,32位被扩展成了64位,具体缘由暂时不明,知道的朋友请赐教)
三、 sint32类型:存数据时引入zigzag编码(Zigzag(n) = (n << 1) ^ (n >> 31), n 为 sint32 时,去掉了符号转为正数),目的是解决负数太占空间的问题。正负数最多占用5个字节,内存高效。
四、 fixed32类型:固定使用4个字节,即正负数一定占用4个字节。由于抛弃了可变长存储的策略。适合用于存储数据大值占比多的字段。
64位的规律与32相似,再也不赘述。
测试代码以下:
@Test public void testStr() { System.out.println("=================string================"); convertStr(""); convertStr("a"); convertStr("abc"); convertStr("啊"); convertStr("啊啊"); }
运行结果以下:
=================string================ 序列化后的数据:[],字节个数:0 a序列化后的数据:[82, 1, 97],字节个数:3 abc序列化后的数据:[82, 3, 97, 98, 99],字节个数:5 啊序列化后的数据:[82, 3, -27, -107, -118],字节个数:5 啊啊序列化后的数据:[82, 6, -27, -107, -118, -27, -107, -118],字节个数:8
【小结】
string类型:proto3中字符串默认为值为空字符串,序列化后不占用内存空间;单个英文字符占1个字节,单个中文字符占3个字节(proto采用utf-8编码)。
测试代码以下:
@Test public void testbool() { System.out.println("=================bool================"); convertBool(false); convertBool(true); }
运行结果以下:
=================bool================ false序列化后的数据:[],字节个数:0 true序列化后的数据:[72, 1],字节个数:2
【小结】
bool类型:proto3中布尔值默认为值为fasle,所以当值为false时,序列化后不占用内存空间;当布尔值为true时,占用1个字节。
浮点型数据都采用的定长编码,其自己没有测试的必要,但在实际应用中,不少浮点型数据(好比经纬度坐标)其实能够转化为必定精度的整数的(容许必定的精度损失),在该场景下,是使用整数型好仍是继续使用浮点型好呢?
测试代码以下:
public void convertAndValiddInt(long value) { //test中其余相似方法定义与其类似,只须要改变set和get方法 //1.经过build建立消息构造器 MyTest.Data.Builder dataBuilder = MyTest.Data.newBuilder(); //2.设置字段值 dataBuilder.setInt64(value); //3.经过消息构造器构造消息对象 MyTest.Data data = dataBuilder.build(); //4.序列化 byte[] bytes = data.toByteArray(); System.out.println(value+"序列化后的数据:" + Arrays.toString(bytes)+",字节个数:"+bytes.length); //5.反序列化 try { MyTest.Data parseFrom = MyTest.Data.parseFrom(bytes); System.out.println("反序列化后的数据="+parseFrom.getInt64()); } catch (InvalidProtocolBufferException e) { e.printStackTrace(); } } @Test public void test(){ System.out.println("================若保留7位小数(精确到厘米)==============="); System.out.println("--> 转为整数,用int64编码:"); convertAndValiddInt(1700000001); System.out.println("--> 仍用小数,用float编码:"); convertAndValiddFloat(170.0000001f); System.out.println("--> 仍用小数,用double编码:"); convertAndValiddDouble(170.0000001); System.out.println("================若保留8位小数(精确到毫米)==============="); System.out.println("--> 转为整数,用int64编码:"); convertAndValiddInt(Long.valueOf("17000000001")); System.out.println("--> 仍用小数,用float编码:"); convertAndValiddFloat(170.00000001f); System.out.println("--> 仍用小数,用double编码:"); convertAndValiddDouble(170.00000001); }
运行结果以下:
================若保留7位小数(精确到厘米)=============== --> 转为整数,用int64编码: 1700000001序列化后的数据:[32, -127, -30, -49, -86, 6],字节个数:6 反序列化后的数据=1700000001 --> 仍用小数,用float编码: 170.0序列化后的数据:[93, 0, 0, 42, 67],字节个数:5 反序列化后的数据=170.0 --> 仍用小数,用double编码: 170.0000001序列化后的数据:[97, -27, -81, 53, 0, 0, 64, 101, 64],字节个数:9 反序列化后的数据=170.0000001 ================若保留8位小数(精确到毫米)=============== --> 转为整数,用int64编码: 17000000001序列化后的数据:[32, -127, -44, -99, -86, 63],字节个数:6 反序列化后的数据=17000000001 --> 仍用小数,用float编码: 170.0序列化后的数据:[93, 0, 0, 42, 67],字节个数:5 反序列化后的数据=170.0 --> 仍用小数,用double编码: 170.00000001序列化后的数据:[97, 100, 94, 5, 0, 0, 64, 101, 64],字节个数:9 反序列化后的数据=170.00000001
【小结】
一、Float表达经纬度有损失(至少保留7位小数的状况下)。
二、对于经纬度等浮点数,将其转为整型数据,用int64编码更省空间。
不少场景会用到时间戳,选用什么类型呢?
测试代码以下:
@Test public void testTime(){ System.out.println("================测试时间戳(精确到秒)==============="); System.out.println("--> 用int64编码:"); convertInt64(Long.valueOf("1600229610283")); System.out.println("--> 用fixed64编码:"); convertFixed64(Long.valueOf("1600229610283")); System.out.println("================测试时间戳(精确到毫秒)==============="); System.out.println("--> 用int64编码:"); convertInt64(Long.valueOf("1600229610283000")); System.out.println("--> 用fixed64编码:"); convertFixed64(Long.valueOf("1600229610283000")); }
运行结果以下:
================测试时间戳(精确到秒)=============== --> 用int64编码: 1600229610283序列化后的数据:[32, -85, -90, -8, -88, -55, 46],字节个数:7 --> 用fixed64编码: 1600229610283序列化后的数据:[65, 43, 19, 30, -107, 116, 1, 0, 0],字节个数:9 ================测试时间戳(精确到毫秒)=============== --> 用int64编码: 1600229610283000序列化后的数据:[32, -8, -65, -21, -21, -25, -20, -21, 2],字节个数:9 --> 用fixed64编码: 1600229610283000序列化后的数据:[65, -8, -33, 122, 125, 102, -81, 5, 0],字节个数:9
【小结】
对于时间戳,建议用int64编码。
对于整型数据:
一、 如有负数,建议使用sint。
二、 若全为正数,则uint、int、sint都可,但sint多算了zigzag编码,增长了计算。建议默认使用int,极可能有负数时用sint。
三、 若大数值占比大,则使用fixed32或fixed64。
对于字符串数据:避免出现中文。
对于时间戳:建议用int64编码。
对于坐标等浮点数:建议将其转为整型数据,用int64编码
【1】https://www.cnblogs.com/lvmf/...
【2】https://www.cnblogs.com/onlys...
【3】https://zhuanlan.zhihu.com/p/...