上一篇文章通過一個例子大致瞭解了protobuf的作用,我曾經打開那個存儲對象編碼後的文件,裏面像是有一團亂碼:
這篇文章主要研究protobuf是如何編碼的,同時你也能感受到protobuf爲什麼更快更省帶寬。
Base 128 Varints
在開始研究過程之前,必須先要了解Varints,Varints提供了一種辦法能讓一個或者多個字節代表整型變量,通常在java中一個int需要佔用4個字節,即使數字1也需要4個字節,而使用Varints能用更少的字節代表比較小的數字,這樣做的目的就是爲了減少編碼後使用的空間,畢竟整型很常用,使用Varints帶來的提高還是很客觀的。瞭解Varints的作用後,下面介紹一下它是怎麼做到的。
比如數字149通過Varints編碼後變成了
10010101 00000001
這裏面有2個字節,在Varints中每個字節的第一個bit都是代表後面還有沒有更多的字節,從上面的例子能看出,第一個字節首bit是1,代表後面還有字節,需要繼續處理,第二個的首bit是0,代表到此爲止後面沒有字節了。
還有一個需要注意的是,Varints採用的是 least significant group first, 網上沒有找到合適的翻譯,其實就是它在表示整型變量時將字節順序反過來存儲,比如上面這個例子:
10010101 00000001 -> 0010101 0000001 //去掉首bit
0010101 0000001 ->00000010010101 //反轉順序
00000010010101換算成10進制就是149
編碼消息包含哪些元素
假設現在有一個消息實體定義爲如下:
message Message { int32 a = 1; }
上面的Message裏只有一個整型變量a, tag爲1. 如果現在將一個a爲149的Message寫入文件,然後從文件中按照字節讀出剛剛寫入的Message內容:
00001000 10010101 00000001
同樣的149和上面介紹的相比,多了一個字節00001000, 這個是什麼作用呢?
protobuf 的消息編碼都是按照多個key-value對來存儲的,既然上面的149是value, 那多出來的字節肯定就是key了,而在protobuf中一個key包含2部分
- field number 也就是上面所說的tag,在這個例子中也就是1
- wire type通俗的講就是類型名稱
常用的wire type如下,這個例子中我們的wire type是0
Type Meaning Used For
0 Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages, packed repeated fields
3 Start group groups (deprecated)
4 End group groups (deprecated)
5 32-bit fixed32, sfixed32, float
而在protobuf中key的計算方式是 key = (field_number << 3) | wire_type, 也就是 1<<3 | 0 即 0001000
你可以理解爲key中的後三位就是wire type
實戰
準備有兩個字段的Message,一個整型一個字符串類型
syntax = "proto3"; package tutorial; option java_package = "com.example.tutorial"; option java_outer_classname = "TestMessage"; message Message { int32 a = 1; string query = 2; }
通過前面文章提到的maven插件生成對象代碼TestMessage.java, 新建一個對象並寫入文件中:
TestMessage.Message.Builder message = TestMessage.Message.newBuilder(); message.setA(149); message.setQuery("zack"); // Write the new address book back to disk. FileOutputStream output = new FileOutputStream("testmessage.txt"); message.build().writeTo(output); output.close();
然後從上面的文件中讀出字節流:
File file = new File(fileName); InputStream in = null; try { in = new FileInputStream(file); int tempbyte; while ((tempbyte = in.read()) != -1) { System.out.print(Integer.toBinaryString(tempbyte)+" "); } in.close(); } catch (IOException e) { e.printStackTrace(); return; }
得出的結果如下:
1000 10010101 1 10010 100 1111010 1100001 1100011 1101011
確實挺長,我們可以一點一點解刨,前3個字節和上面的例子一樣1000 10010101 1,就是149的整型變量,那來看看後面的6個字節:
10010 100 1111010 1100001 1100011 1101011
- 第一個字節 10010:後三位010即wire type是2, 吻合; 剩下的2個字節10即field_number, 也吻合
- 第二個字節100: 由於這個wire type是2,因此這個字節代表的是後面的字節有幾個,也就是4個字符,一個字符佔用一個字節,而我們代碼裏存的是zack, 確實是4個字節
- 剩下的4個字節毫無疑問就是zack四個字符的Ascii的值, 注意這裏不是用Varints那種方式去解析,還記得Varints的應用範圍嗎?它是用來表示整型變量的,這裏是字符。
總結
現在回想一下整個過程,你是否發現protobuf在使用盡量少的字節去表達儘量多的含義,包括減少整型變量的空間佔用以及在表示變量類型和field_num時只使用少的字節數,同樣的消息,編碼佔用的空間越少,則它傳輸也就越快。
至此,我們研究了protobuf是如何將一個對象的屬性編碼成一個字節流的過程,如果你還想知道其他類型的字段是如何編碼的,可以參考protobuf的官網,這裏就不細講了,原理都是差不多的。
另附文章中提到的工程文件:
歡迎關注我的個人的博客www.zhijianliu.cn, 虛心求教,有錯誤還請指正輕拍,謝謝
版權聲明:本文出自志健的原創文章,未經博主允許不得轉載