天天看點

[轉] Protobuf高效結構化資料存儲格式

從公司的項目源碼中看到了這個東西,覺得挺好用的,寫篇部落格做下小總結。下面的操作以C++為程式設計語言,protoc的版本為libprotoc 3.2.0。

一、Protobuf? 

1. 是什麼? 

  Google Protocol Buffer(簡稱 Protobuf)是一種輕便高效的結構化資料存儲格式,平台無關、語言無關、可擴充,可用于通訊協定和資料存儲等領域。

2. 為什麼要用?

  - 平台無關,語言無關,可擴充;

  - 提供了友好的動态庫,使用簡單;

  - 解析速度快,比對應的XML快約20-100倍;

  - 序列化資料非常簡潔、緊湊,與XML相比,其序列化之後的資料量約為1/3到1/10。

3. 怎麼安裝? 

  源碼下載下傳位址: GitHub - protocolbuffers/protobuf: Protocol Buffers - Google's data interchange format 

  安裝依賴的庫: autoconf automake libtool curl make g++ unzip  

安裝:

1 $ ./autogen.sh
2 $ ./configure
3 $ make
4 $ make check
5 $ sudo make install      

二、怎麼用? 

1. 編寫proto檔案 

  首先需要一個proto檔案,其中定義了我們程式中需要處理的結構化資料:

[轉] Protobuf高效結構化資料存儲格式
1 // Filename: addressbook.proto
 2 
 3 syntax="proto2";
 4 package addressbook;
 5 
 6 import "src/help.proto";      //舉例用,編譯時去掉
 7 
 8 message Person {
 9     required string name = 1;
10     required int32 id = 2;
11     optional string email = 3;
12 
13     enum PhoneType {
14         MOBILE = 0;
15         HOME = 1;
16         WORK = 2;
17     }
18 
19     message PhoneNumber {
20         required string number = 1;
21         optional PhoneType type = 2 [default = HOME];
22     }
23 
24     repeated PhoneNumber phone = 4;
25 }
26 
27 message AddressBook {
28     repeated Person person_info = 1;
29 }      
[轉] Protobuf高效結構化資料存儲格式

2. 代碼解釋

 // Filename: addressbook.proto 這一行是注釋,文法類似于C++ 

 syntax="proto2"; 表明使用protobuf的編譯器版本為v2,目前最新的版本為v3 

 package addressbook; 聲明了一個包名,用來防止不同的消息類型命名沖突,類似于 namespace 

 import "src/help.proto";  導入了一個外部proto檔案中的定義,類似于C++中的 include 。不過好像隻能import目前目錄及目前目錄的子目錄中的proto檔案,比如import父目錄中的檔案時編譯會報錯(Import "../xxxx.proto" was not found or had errors.),使用絕對路徑也不行,尚不清楚原因,官方文檔說使用 -I=PATH 或者 --proto_path=PATH 來指定import目錄,但實際實驗結果表明這兩種方式指定的是将要編譯的proto檔案所在的目錄,而不是import的檔案所在的目錄。(哪位大神若清楚還請不吝賜教!) 

 message 是Protobuf中的結構化資料,類似于C++中的類,可以在其中定義需要處理的資料 

 required string name = 1; 聲明了一個名為name,資料類型為string的required字段,字段的辨別号為1 

protobuf一共有三個字段修飾符: 

  - required:該值是必須要設定的; 

  - optional :該字段可以有0個或1個值(不超過1個); 

  - repeated:該字段可以重複任意多次(包括0次),類似于C++中的list;

使用建議:除非确定某個字段一定會被設值,否則使用optional代替required。 

 string 是一種标量類型,protobuf的所有标量類型請參考文末的标量類型清單。 

 name 是字段名,1 是字段的辨別号,在消息定義中,每個字段都有唯一的一個數字辨別号,這些辨別号是用來在消息的二進制格式中識别各個字段的,一旦開始使用就不能夠再改變。 

辨別号的範圍在:1 ~ 229 - 1,其中[19000-19999]為Protobuf預留,不能使用。

 Person 内部聲明了一個enum和一個message,這類似于C++中的類内聲明,Person外部的結構可以用 Person.PhoneType 的方式來使用PhoneType。當使用外部package中的結構時,要使用 pkgName.msgName.typeName 的格式,每兩層之間使用'.'來連接配接,類似C++中的"::"。 

 optional PhoneType type = 2 [default = HOME]; 為type字段指定了一個預設值,當沒有為type設值時,其值為HOME。 

另外,一個proto檔案中可以聲明多個message,在編譯的時候他們會被編譯成為不同的類。

3. 生成C++檔案 

  protoc是proto檔案的編譯器,目前可以将proto檔案編譯成C++、Java、Python三種代碼檔案,編譯格式如下:

1 protoc -I=$SRC_DIR --cpp_out=$DST_DIR /path/to/file.proto      

上面的指令會生成xxx.pb.h 和 xxx.pb.cc兩個C++檔案。

4. 使用C++檔案

  現在編寫一個main.cc檔案:

[轉] Protobuf高效結構化資料存儲格式
1 #include <iostream>
 2 #include "addressbook.pb.h"
 3 
 4 int main(int argc, const char* argv[])
 5 {
 6     addressbook::AddressBook person;
 7     addressbook::Person* pi = person.add_person_info();
 8 
 9     pi->set_name("aut");
10     pi->set_id(1219);
11     std::cout << "before clear(), id = " << pi->id() << std::endl;
12     pi->clear_id();
13     std::cout << "after  clear(), id = " << pi->id() << std::endl;
14     pi->set_id(1087);
15     if (!pi->has_email())
16         pi->set_email("[email protected]");
17 
18     addressbook::Person::PhoneNumber* pn = pi->add_phone();
19     pn->set_number("021-8888-8888");
20     pn = pi->add_phone();
21     pn->set_number("138-8888-8888");
22     pn->set_type(addressbook::Person::MOBILE);
23 
24     uint32_t size = person.ByteSize();
25     unsigned char byteArray[size];
26     person.SerializeToArray(byteArray, size);
27 
28     addressbook::AddressBook help_person;
29     help_person.ParseFromArray(byteArray, size);
30     addressbook::Person help_pi = help_person.person_info(0);
31 
32     std::cout << "*****************************" << std::endl;
33     std::cout << "id:    " << help_pi.id() << std::endl;
34     std::cout << "name:  " << help_pi.name() << std::endl;
35     std::cout << "email: " << help_pi.email() << std::endl;
36 
37     for (int i = 0; i < help_pi.phone_size(); ++i)
38     {
39         auto help_pn = help_pi.mutable_phone(i);
40         std::cout << "phone_type: " << help_pn->type() << std::endl;
41         std::cout << "phone_number: " << help_pn->number() << std::endl;
42     }
43     std::cout << "*****************************" << std::endl;
44 
45     return 0;
46 }       
[轉] Protobuf高效結構化資料存儲格式

5. 常用API

  protoc為message的每個required字段和optional字段都定義了以下幾個函數(不限于這幾個):

1 TypeName xxx() const;          //擷取字段的值
2 bool has_xxx();              //判斷是否設值
3 void set_xxx(const TypeName&);   //設值
4 void clear_xxx();          //使其變為預設值      

為每個repeated字段定義了以下幾個:

1 TypeName* add_xxx();        //增加結點
2 TypeName xxx(int) const;    //擷取指定序号的結點,類似于C++的"[]"運算符
3 TypeName* mutable_xxx(int); //類似于上一個,但是擷取的是指針
4 int xxx_size();            //擷取結點的數量      

另外,下面幾個是常用的序列化函數:

1 bool SerializeToOstream(std::ostream * output) const; //輸出到輸出流中
2 bool SerializeToString(string * output) const;        //輸出到string
3 bool SerializeToArray(void * data, int size) const;   //輸出到位元組流      

與之對應的反序列化函數:

1 bool ParseFromIstream(std::istream * input);     //從輸入流解析
2 bool ParseFromString(const string & data);       //從string解析
3 bool ParseFromArray(const void * data, int size); //從位元組流解析      

其他常用的函數:

1 bool IsInitialized();    //檢查是否所有required字段都被設值
2 size_t ByteSize() const; //擷取二進制位元組序的大小      

官方API文檔位址: https://developers.google.com/protocol-buffers/docs/reference/overview

6. 編譯生成可執行代碼

  編譯格式和普通的C++代碼一樣,但是要加上 -lprotobuf -pthread 

1 g++ main.cc xxx.pb.cc -I $INCLUDE_PATH -L $LIB_PATH -lprotobuf -pthread       

7. 輸出結果

[轉] Protobuf高效結構化資料存儲格式
1 before clear(), id = 1219
 2 after  clear(), id = 0
 3 *****************************
 4 id:   1087
 5 name: aut
 6 email: [email protected]
 7 phone_type: 1
 8 phone_number: 021-8888-8888
 9 phone_type: 0
10 phone_number: 138-8888-8888
11 *****************************      
[轉] Protobuf高效結構化資料存儲格式

三、怎麼編碼的?

  protobuf之是以小且快,就是因為使用變長的編碼規則,隻儲存有用的資訊,節省了大量空間。

1. Base-128變長編碼

  - 每個位元組使用低7位表示數字,除了最後一個位元組,其他位元組的最高位都設定為1;

  - 采用Little-Endian位元組序。

示例:

[轉] Protobuf高效結構化資料存儲格式
1 -數字1:
2 0000 0001
3 
4 -數字300:
5 1010 1100 0000 0010
6 000 0010 010 1100
7 -> 000 0010 010 1100
8 -> 100101100
9 -> 256 + 32 + 8 + 4 = 300      
[轉] Protobuf高效結構化資料存儲格式

2. ZigZag編碼

  Base-128變長編碼會去掉整數前面那些沒用的0,隻保留低位的有效位,然而負數的補碼表示有很多的1,是以protobuf先用ZigZag編碼将所有的數值映射為無符号數,然後使用Base-128編碼,ZigZag的編碼規則如下:

1 (n << 1) ^ (n >> 31) or (n << 1) ^ (n >> 63)      

負數右移後高位全變成1,再與左移一位後的值進行異或,就把高位那些無用的1全部變成0了,巧妙!

3. 消息格式

  每一個Protocol Buffers的Message包含一系列的字段(key/value),每個字段由字段頭(key)和字段體(value)組成,字段頭由一個變長32位整數表示,字段體由具體的資料結構和資料類型決定。 

字段頭格式:

1 (field_number << 3) | wire_type
2 -field_number:字段序号
3 -wire_type:字段編碼類型      

4. 字段編碼類型

Type Meaning Used For
Varint int32, int64, uint32, uint64, sint32, sint64, bool, enum
1 64-bit fixed64, sfixed64, double
2 Length-delimited string, bytes, embedded messages(嵌套message), packed repeated fields
3 Start group groups (廢棄) 
4 End group groups (廢棄)
5 32-bit fixed32, sfixed32, float

 5. 編碼示例(下面的編碼以16進制表示)

[轉] Protobuf高效結構化資料存儲格式
1 示例1(整數)
 2 message Test1 {
 3     required int32 a = 1;
 4 }
 5 a = 150 時編碼如下
 6 08 96 01
 7 08: 1 << 3 | 0
 8 96 01:
 9 1001 0110 0000 0001
10 -> 001 0110 000 0001
11 -> 1001 0110
12 -> 150
13 
14 示例2(字元串)
15 message Test2 {
16     required string b = 2;
17 }
18 b = "testing" 時編碼如下
19 12 07 74 65 73 74 69 6e 67
20 12: 2 << 3 | 2
21 07: 字元串長度
22 74 65 73 74 69 6e 67
23 -> t e s t i n g
24 
25 示例3(嵌套)
26 message Test3 {
27     required Test1 c = 3;
28 }
29 c.a = 150 時編碼如下
30 1a 03 08 96 01
31 1a: 3 << 3 | 2
32 03: 嵌套結構長度
33 08 96 01
34 ->Test1 { a = 150 }
35 
36 示例4(可選字段)
37 message Test4 {
38     required int32 a = 1;
39     optional string b = 2;
40 }
41 a = 150, b不設值時編碼如下
42 08 96 01
43 -> { a = 150 }
44 
45 a = 150, b = "aut" 時編碼如下
46 08 96 01 12 03 61 75 74
47 08 96 01 -> { a = 150 }
48 12: 2 << 3 | 2
49 03: 字元串長度
50 61 75 74
51 -> a u t
52 
53 示例5(重複字段)
54 message Test5 {
55     required int32 a = 1;
56     repeated string b = 2;
57 }
58 a = 150, b = {"aut", "honey"} 時編碼如下
59 08 96 01 12 03 61 75 74 12 05 68 6f 6e 65 79
60 08 96 01 -> { a = 150 }
61 12: 2 << 3 | 2
62 03: strlen("aut") 
63 61 75 74 -> a u t
64 12: 2 << 3 | 2
65 05: strlen("honey")
66 68 6f 6e 65 79 -> h o n e y
67 
68 a = 150, b = "aut" 時編碼如下
69 08 96 01 12 03 61 75 74
70 08 96 01 -> { a = 150 }
71 12: 2 << 3 | 2
72 03: strlen("aut") 
73 61 75 74 -> a u t
74 
75 示例6(字段順序)
76 message Test6 {
77     required int32 a = 1;
78     required string b = 2;
79 }
80 a = 150, b = "aut" 時,無論a和b誰的聲明在前面,編碼都如下
81 08 96 01 12 03 61 75 74
82 08 96 01 -> { a = 150 }
83 12 03 61 75 74 -> { b = "aut" }      
[轉] Protobuf高效結構化資料存儲格式

四、還有什麼?

1. 編碼風格 

  - 花括号的使用(參考上面的proto檔案)

  - 資料類型使用駝峰命名法:AddressBook, PhoneType

  - 字段名小寫并使用下劃線連接配接:person_info, email_addr

  - 枚舉量使用大寫并用下劃線連接配接:FIRST_VALUE, SECOND_VALUE

2. 适用場景

  "Protocol Buffers are not designed to handle large messages."。protobuf對于1M以下的message有很高的效率,但是當message是大于1M的大塊資料時,protobuf的表現不是很好,請合理使用。

總結:本文介紹了protobuf的基本使用方法和編碼規則,還有很多内容尚未涉及,比如:反射機制、擴充、Oneof、RPC等等,更多内容需參考官方文檔。

标量類型清單

proto類型 C++類型 備注
double double
float float
int32 int32 使用可變長編碼,編碼負數時不夠高效——如果字段可能含有負數,請使用sint32
int64 int64 使用可變長編碼,編碼負數時不夠高效——如果字段可能含有負數,請使用sint64
uint32 uint32 使用可變長編碼
uint64 uint64 使用可變長編碼
sint32 int32 使用可變長編碼,有符号的整型值,編碼時比通常的int32高效
sint64 int64 使用可變長編碼,有符号的整型值,編碼時比通常的int64高效
fixed32 uint32 總是4個位元組,如果數值總是比總是比228大的話,這個類型會比uint32高效
fixed64 uint64 總是8個位元組,如果數值總是比總是比256大的話,這個類型會比uint64高效
sfixed32 int32 總是4個位元組
sfixed64 int64 總是8個位元組
bool bool
string string 一個字元串必須是UTF-8編碼或者7-bit ASCII編碼的文本
bytes string 可能包含任意順序的位元組資料

顯示詳細資訊

參考資料 

1. Protocol Buffers Developer Guide

2. Google Protocol Buffer 的使用和原理

3. 淺談幾種序列化協定

4. 序列化和反序列化

5. Protobuf使用手冊

原文連結:

https://www.cnblogs.com/autyinjing/p/6495103.html