【Protobuf專題】(二)Protobuf的數據類型解析及使用總結

0 前言

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...

1 基本數據類型的範圍

整型範圍java

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

無符號整型範圍segmentfault

  • 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...

image.png

注意到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

image.png

3 數據實驗

爲驗證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

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

【小結】
一、 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相似,再也不贅述。

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

【小結】
一、Float表達經緯度有損失(至少保留7位小數的狀況下)。
二、對於經緯度等浮點數,將其轉爲整型數據,用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 總結

對於整型數據:

一、 如有負數,建議使用sint。
二、 若全爲正數,則uint、int、sint都可,但sint多算了zigzag編碼,增長了計算。建議默認使用int,極可能有負數時用sint。
三、 若大數值佔比大,則使用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...

相關文章
相關標籤/搜索