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