乐趣区

关于protobuf:Protobuf专题二Protobuf的数据类型解析及使用总结

0 前言

Protobuf(Protocol Buffer)是 Google 出品的一种轻量且高效的结构化数据存储格局,性能比 Json、XML 更强,被广泛应用于数据传输中。然 Protobuf 中的数据类型泛滥,什么场景利用什么数据类型最正当、最省空间,成为了每个使用者该思考的问题。为了能更充沛的了解和应用 Protobuf,本文将聚焦 Protobuf 的根本数据类型,剖析其不同数据类型的应用场景和注意事项。

留神:在浏览本文之前最好对 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…

1 根本数据类型的范畴

整型范畴

  • Int8 – [-128 : 127]
  • Int16 – [-32768 : 32767]
  • Int32 – [-2147483648 : 2147483647]
  • Int64 – [-9223372036854775808 : 9223372036854775807]

无符号整型范畴

  • UInt8 – [0 : 255]
  • UInt16 – [0 : 65535]
  • UInt32 – [0 : 4294967295]
  • UInt64 – [0 : 18446744073709551615]

浮点数范畴

  • Float(32bit)= 1bit(符号位)+ 8bits(指数位)+ 23bits(尾数位)
    指数位的范畴为 -2^128 ~ +2^128
    尾数位的范畴为 2^23 = 8388608,一共七位,这意味着最多能有 7 位有效数字,但相对能保障的为 6 位,也即 float 的精度为 6~7 位有效数字;
  • Double(64bit)= 1bit(符号位)+ 11bits(指数位)+ 52bits(尾数位)
    指数位的范畴为 -2^1024 ~ +2^1024
    尾数位的范畴为 2^52 = 4503599627370496,一共 16 位,同理,double 的精度为 15~16 位。

浮点数的存储形式详见:https://cloud.tencent.com/dev…

2 Protobuf 的数据类型

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),这种办法次要针对相似于数组的数据,增加了一个字段记录数组的长度,而后将数组内容程序组合,具体原理不在赘述,可见前文举荐的文献。

3 数据试验

为验证 Protobuf 各数据类型的序列化成果,遂设计以下数据试验。

1、首先,自定义了 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;
}

2、其次,别离对每个数据类进行赋不同的值并序列化,察看不同数据序列化后占用的字节数。
3、最初,总结演绎,造成应用倡议。

3.1 整数型数据试验

测试代码如下:

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

【小结】
1、uint32 类型 :数值范畴等价于 int32 的范畴(能够存正数,因为 proto 没有对正数进行判断及限度)。 正数最多占用 5 个字节,正数必占用 5 个字节。(第一个字节存储的是数据类型和字段在 proto 中的编号,即原理篇里讲的 tag。之所以 32 位的数据最多要用 5 个字节来存储,是因为每个字节的最高位须要记录该数据是否衍生到下个字节(为实现可变长存储),1 示意衍生,0 示意不衍生。所以每个字节的理论存储数据的位数为 7,则 4 *7<32,因而须要 5 个字节)

2、int32 类型:存负数时最多须要 5 个字节,存正数时必然须要 10 个字节。(因为存正数时,32 位被扩大成了 64 位,具体起因临时不明,晓得的敌人请赐教)

3、sint32 类型 :存数据时引入 zigzag 编码(Zigzag(n) = (n << 1) ^ (n >> 31), n 为 sint32 时,去掉了符号转为负数),目标是解决正数太占空间的问题。 正负数最多占用 5 个字节,内存高效

4、fixed32 类型 :固定应用 4 个字节, 即正负数必然占用 4 个字节 。因为摈弃了可变长存储的策略。 适宜用于存储数据大值占比多的字段

64 位的法则与 32 相似,不再赘述。

3.2 字符串类型数据试验

测试代码如下:

@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 编码)。

3.3 布尔值类型数据试验

测试代码如下:

@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 个字节。

3.4 浮点型数据试验

浮点型数据都采纳的定长编码,其自身没有测试的必要,但在理论利用中,很多浮点型数据(比方经纬度坐标)其实能够转化为肯定精度的整数的(容许肯定的精度损失),在该场景下,是应用整数型好还是持续应用浮点型好呢?

测试代码如下:

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

【小结】
1、Float 表白经纬度有损失(至多保留 7 位小数的状况下)。
2、对于经纬度等浮点数,将其转为整型数据,用 int64 编码更省空间。

3.5 工夫戳数据试验

很多场景会用到工夫戳,选用什么类型呢?
测试代码如下:

@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 编码。

4 总结

对于整型数据:

1、若有正数,倡议应用 sint。
2、若全为负数,则 uint、int、sint 均可,但 sint 多算了 zigzag 编码,减少了计算。倡议默认应用 int,很可能有正数时用 sint。
3、若大数值占比大,则应用 fixed32 或 fixed64。

对于字符串数据:避免出现中文。

对于工夫戳:倡议用 int64 编码。

对于坐标等浮点数:倡议 将其转为整型数据,用 int64 编码

5 参考文献

【1】https://www.cnblogs.com/lvmf/…

【2】https://www.cnblogs.com/onlys…

【3】https://zhuanlan.zhihu.com/p/…

【4】https://www.zhihu.com/questio…

【5】https://developers.google.com…

退出移动版