【轉載】Protobuf原理分析小結

參考資料:
https://blog.csdn.net/zxhoo/article/details/53228303
https://blog.csdn.net/carson_ho/article/details/70568606

什麼是Protobuf

Protobuf是一個網絡通信協議,提供了高效率的序列化和反序列化機制,序列化就是把對象轉換成二進制數據發送給服務端,反序列化就是將收到的二進制數據轉換成對應的對象。

Protobuf消息結構

在這裏插入圖片描述
使用以Java爲例:

byte[] data = Test.newBuilder()
  .setA(3).setB(2).setC(1)
  .build().toByteArray();

上面對Protobuf的定義,比如:

int32 a = 1;

這個定義不是賦值,它只是定義了a字段的tag,tag包含了數據類型(int32)和字段序號(1),真正的賦值的在使用的時候,比如上面的Java代碼。
序列化之後的數據相當於Key-Value形式,T-V:
在這裏插入圖片描述
其中,tag有三個作用,一個保證字段不重複,二是保證它是數據流中的位置,三是標記了數據類型。所以,tag是由fieldNumber和wireType組成,fieldNumber保證了字段不重複和它是數據流中的位置,wireType標記了數據類型。

Protobuf優點

  • 體積小、效率高;
  • 使用簡單、兼容性好、維護簡單;
  • 加密性好;
  • 跨平臺。

兼容性好

使用Json的時候,有這麼一種情況,某個字段值爲null或者某個key爲null時,Android或IOS相應的Json解析庫可能會報錯,而Protobuf很好的解決了這問題。

比如,Json序列化的時候,二進制信息如下:
在這裏插入圖片描述
這種定義,可以對數據順序寫入,然後再順序讀取,這樣帶來一個問題就是,某些字段沒有賦值的情況下,不得不傳一個默認值,假如field2沒有賦值,那麼整個解析包偏移量都會出錯,最終整個包的數據讀不出。

而Protobuf引入了Tag,解決了這個問題:
在這裏插入圖片描述
每個field都是由tag和value組成,解析的時候,先讀tag,然後通過tag知道value的數據類型,再獲取value,寫的時候也是一樣,先寫入tag再寫入value。

因爲每個field都定義了tag,如果field沒有賦值,編碼的時候tag不會被寫入流中,相應的也不會有它的Value,相對應的解析的時候,如果數據中沒有這個field的tag,可以直接無視,讀取其他field。

比如上述的常規定義的的二進制信息,在field2沒有賦值的情況下,protobuf可以這樣:
在這裏插入圖片描述
還有另外一種兼容的情況,比如:message需要增加一個字段,如果客戶端沒有升級,服務端升級了,這個時候客戶端是舊的message,服務端用的是新的message。

客戶端的舊的message:
在這裏插入圖片描述
服務端的新的message:
在這裏插入圖片描述
這樣子,客戶端接收到服務端發送過來的數據流是這樣的:
在這裏插入圖片描述
而當客戶端解析這數據的時候,發現數據流裏面有個tag爲3,但是在客戶端的協議裏找不到對應的tag,然後通過數據流這個tag,這點了它是數據類型是int64,所以知道了這個tag的值佔了8個字節,於是Protobuf就會跳過這8個字節,繼續解析後面的數據。

效率高、體積小

Protobuf之所以效率比Json、XML高,是因爲內部採用了很巧妙的編碼方式,來達到數據壓縮的目的,比如:
對於 int32類型的數字,一般需要4個字節表示,比如1和300:

00000000 00000000 00000000 00000001 //1
00000000 00000000 00000001 00101100 //300

而通過Protobuf壓縮之後是這樣的:

00000001 //1
10101100 00000010 //300

Varint編碼

原理:值越小的數字,使用越少的字節數表示
作用:通過減少表示數字的字節數從而進行數據壓縮
Varint編碼高位有特殊含義:如果是1,表示後續的字節也是該數字的一部分,如果是0,表示這是最後一個字節,且剩餘7位都用來表示數字。

舉例下Varint編碼和解碼過程:
客戶端發送300給服務端,通過Protobuf編碼過程:
首先300的源數據是:

00000000 00000000 00000001 00101100

前面兩個字節沒有意義,Varint會丟掉前面兩字節,這裏標記爲字節0變成:

 00000001 00101100

然後從字節0的尾部開始,取7位,變成新字節1,並在最高位補1,最高位補1還是0,取決於後面還有沒有字節,字節1爲:

10101100

然後繼續在字節0中取7位,標記爲字節2,由於這次取完後面已經沒有字節了,所以字節2高位爲0:

00000010

最後,最終編碼後的數據變成字節1+字節2:

10101100 00000010

以上就完成了Varint對300編碼,接下來看下服務端接收到編碼後的數據怎麼解析:
首先,接收到的數據是:

10101100 00000010

首先分析下這段數據,有兩個字節,每個字節的最高位只是標記的作用,1代表後面的字節是數字的一部分,0表示這個字節是最後一個字節了,所以去掉各自的最高位,變成:

0101100 0000010

然後Varint會將字節調轉,變成:

0000010 0101100

對比300的源數據:

 0000010 0101100
 00000000 00000000 00000001 00101100

調轉後的數據:256+32+8+4 = 300

實際例子

定義Protobuf:

message person
 { 
    int32   id = 1;  
    // wire type = 0,field_number =1 
    string  name = 2;  
    // wire type = 2,field_number =2 
  }
person.setId(1);
person.setName("testing");

上面的wire type的值,Protobuf內部已經定義好了:
在這裏插入圖片描述
首先分析字段 int32 id = 1:

由於是int32類型,所以數據是tag-value形式:

tag:

表達式:Tag = (field_number << 3) | wire_type
字段int32 id = 1的field_number爲1,左移三位變成:

00001000

然後數據類型是int32,所以 wire_type爲0,則最終得出的tag爲:

00001000

value:

根據Varint編碼,變成1字節:

00000001

最終

字段 int32 id = 1變成:

00001000 00000001    //即8 和 1

然後分析字段 string name = 2:

由於是string類型,所以是tag-length-value形式,value採用UTF編碼

tag:

field_number爲2,左移3位:

00010000

string類型wire type爲2,最終tag爲:

00010010

Value:

上面的例子是字符串testing,經過UTF8編碼後,變成:

116101115116105110103

Length

value的長度,所以是7,即00000111

最終

數據爲:

00010010  00000111  116101115116105110103

總結

序列化、反序列化簡單、速度快的原因是:
編碼 / 解碼 方式簡單(只需要簡單的數學運算 = 位移等等)

數據壓縮效果好(即序列化後的數據量體積小)的原因是:
採用了獨特的編碼方式,如Varint、Zigzag編碼方式等等

兼容性高的原因:
採用T - L - V 的數據存儲方式

作者:Dane_404
鏈接:https://www.jianshu.com/p/522f13206ba1
來源:簡書
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

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