GRPC學習之路(4)——protobuf編碼過程解析

上一篇文章通過一個例子大致瞭解了protobuf的作用,我曾經打開那個存儲對象編碼後的文件,裏面像是有一團亂碼:

這篇文章主要研究protobuf是如何編碼的,同時你也能感受到protobuf爲什麼更快更省帶寬。

Base 128 Varints

在開始研究過程之前,必須先要了解VarintsVarints提供了一種辦法能讓一個或者多個字節代表整型變量,通常在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部分

  1. field number 也就是上面所說的tag,在這個例子中也就是1
  2. 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的官網,這裏就不細講了,原理都是差不多的。

另附文章中提到的工程文件:

Proto3Tutorial

歡迎關注我的個人的博客www.zhijianliu.cn, 虛心求教,有錯誤還請指正輕拍,謝謝

版權聲明:本文出自志健的原創文章,未經博主允許不得轉載

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章