【GoLang 那點事】聊聊 gRPC 的介面描述語言 ProtoBuffer(二)

a_wei發表於2019-08-22

什麼是ProtoBuffer

  • ProtoBuffer是一種與語言無關,平臺無關,可擴充套件的序列化結構化資料的方法,用於通訊協議,資料儲存等,ProtoBuffer由Google開發,目前各大網際網路公司普遍使用,在使用時需要編寫.proto檔案,目前ProtoBuffer有兩個版本,Pro2、Pro3,這次主要分享的是Pro3。

ProtoBuffer的特點

  • 相比xml,json等資料序列化方式,ProtoBuffer具有如下特點:
  • 體積小3到10倍,(其資料格式緊密,沒有多餘的空格,括號,尖括號,key等)
  • 效能快20到100倍(體積小了,所以傳輸也快,另外protobuffer也做了一個額外處理,比如傳入每個欄位值的長度,方便讀取)
  • 生成更易於以程式設計方式使用的資料訪問類
  • 支援新欄位增加,向後相容
  • 支援相對複雜的資料格式
  • 跨語言(為每種語言提供了編譯器),跨平臺(序列化結果為二進位制與平臺無關)
  • .proto檔案可讀性不高,序列化後的位元組序列為二進位制序列,不能簡單的分析有效性

ProtoBuffer安裝

ProtoBuffer的資料型別和各語言的資料型別對應關係

Pro3 Java Python Go
double double float float64
float float float float32
int32 int int int32
int64 long int/long[3] int64
uint32 int[1] int/long[3] uint32
uint64 long[1] int/long[3] uint64
sint32 int int int32
sint64 long int/long[3] int64
fixed32 int[1] int/long[3] uint32
fixed64 long[1] int/long[3] uint64
sfixed32 int int int32
sfixed64 long int/long[3] int64
bool boolean bool bool
string String str/unicode[4] string
bytes byteSring str []byte

ProtoBuffer的使用

  • 我們建立一個person.proto檔案來描述人的一些資訊
//宣告proto的版本,並且必須是第一行,否則認為是proto2版本
syntax = "proto3";

//最終通過編譯器生成的.go檔案的包名
package proto_file;

//使用message定義Person結構體,按照上面的型別對映一一對映
message Person{    
    string no = 1;    
    string name = 2;    
    int32  age = 3;    
    int32  sex = 4;    
    enum PhoneType{        
        HOME = 0;        
        WORK = 1;        
        MOBILE = 2;    
    }    
    message PhoneNumber{        
        string number = 1;        
        PhoneType type = 2;    
    }    
    repeated PhoneNumber phones = 5;    
    repeated  string address = 6;}
}
  • 解釋一下上面一些欄位的含義

  • message,類似與Java中的class,go中的struct

  • repeated代表這個欄位是可以重複出現的,對應的就是類似陣列型別

  • 每個欄位後面的編號代表著欄位在序列化以後二進位制資料中的位置,編號越大越往後,該值在同一message中不能重複

  • enum是列舉型別欄位的關鍵字,等同於Java中的enum,HOME,WORK,MOBILE為列舉值,可以為列舉值指定任意的整型值,整型值的順序必須連續,且在proto3中必須從0開始

  • 下面說一下如何將proto檔案編譯成go檔案

  • 下面我們通過protobuffer提供的外掛來生成對應的person.pb.go檔案

  • 首先下載外掛 go get -u github.com/golang/protobuf/protoc-gen-go

  • 然後執行以下命令生成對應的go檔案

  • protoc -I "proto檔案的路徑" --go_out="生成的go檔案的路徑" route_guide.proto

  • 我我這裡使用的命令是:protoc --go_out=. route_guide.proto

  • 沒有 -I代表我在proto檔案下執行的命令, .代表我最後生成的.pb.go檔案在當前目錄下,如下截圖:
    Golang

  • 最終生成的程式碼如下,擷取一些核心程式碼,我們可以看到protobuffer編譯器將proto檔案能夠轉化為go的struct

Golang

ProtoBuffer的原理

  • 什麼是Base 128 varint?這是一個編碼演算法,我們都知道,int32佔四個位元組,int64佔8個位元組,這是固定的,不管這個數字是1還是123456,佔的位元組數是一樣,那有沒有一種能根據數字大小變長編碼的演算法呢?Base 128 varint就是,在二進位制網路協議通訊時,這種好處是可觀的,能夠帶來效能上的提升。為什麼叫128呢,就是因為採用7bit的空間儲存資料(一個位元組佔8bit,但只採用7bit),7bit最大當然只能儲存128了,那麼最高位幹啥呢?最高位用來當作一個標識(flag),如果最高位是0就表示這個最後一個位元組了。

    • *
  • 示例:我們用一個數字10和數字300來講解一下上面的Base 128 varint

  • 先說數字10,轉化為二進位制後是:0000 1010,為什麼只有八位呢,因為10用一個位元組表示已經足夠了,最高位為0(加粗的那個),表示這是最後一個位元組了,不需要再用額外的位元組來儲存了

  • 再來看數字300,轉化為二進位制後是:‭‭00010010_1100‬,轉化成varint,如下步驟:

  • 按照7位進行分開, 0000010_0101100,不夠的補0

  • 進行反轉:0101100_0000010

  • 最高位補數,第一個位元組最高位補1,第二個位元組最高位補0:10101100_00000010

    • *
  • ProtoBuffer序列化後的儲存格式是什麼樣的呢?

  • Tag,Length,Value ,這是序列化後儲存的二進位制的格式,Tag大家簡單理解為就是proto檔案中欄位後面的編號,Length是這個欄位對應的值的位元組長度,Value就是具體的值了,最終將所有資料拼裝成一個流,如下圖:

Golang

  • 由圖我們得知,ProtoBuffer儲存是緊密的,各個欄位非常緊湊,不會浪費空間,若某個欄位沒有賦值,則不會出現在序列化後的資料中,相應欄位在解碼時才會被設定預設值。

  • ProtoBuffer對不同型別資料採用編碼方式和儲存方式,如下圖:

Golang

  • 如上圖,如果採用varint方式,則儲存的格式是TV格式,沒有L,因為T上就已經知道V的位元組長度了。

  • T代表的tag是由fieldNumber(欄位編號)和wireType(上圖中最左邊的0,1,2...)組成的,fieldNumber保證了欄位不重複和他在資料流中的位置,wireType標記了資料型別,如果是varint便哈,fieldNumber也保證了資料位元組的長度(L)

  • varint編碼的不足

  • 整數1在計算幾儲存中二進位制是0000 00001,那麼你知道整數-1的二進位制呢?如下:

  • ‭11111111_11111111_11111111_11111111‬,如果也採用varint編碼那麼就需要至少佔用5個位元組,這顯然有些浪費空間,ProtoBuffer的解決方案如下:

  • ProtoBuffer定義了sint32和sint64型別來表示負數,通過先採用Zigzag編碼(將由符號數轉化成無符號數),再採用varint編碼,從而用於減少編碼後的位元組數

Golang

  • 什麼是Zigzag編碼?
  • Zigzag也是一種變長的編碼方式,使用無符號數表示有符號數,作用是使得絕對值小的數字可以採用較小子的位元組進行表示,Zigzag編碼是輔助varint在編碼負數時的不足,從而更好的幫助ProtoBuffer進行資料的壓縮,下面一張圖瞭解:

Golang

總結

  • ProtoBuffer編解碼方式簡單(只需要簡單的數學運算,位運算)
  • ProtoBuffer資料壓縮方式好,佔用的空間小
  • ProtoBuffer相容性好,採用TLV的儲存格式

參考文章:

歡迎大家關注微信公眾號:“golang那點事”,更多精彩期待你的到來

Golang

那小子阿偉

相關文章