[轉] Protobuf高效結構化數據存儲格式


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

一、Protobuf? 
1. 是什么? 
  Google Protocol Buffer(簡稱 Protobuf)是一種輕便高效的結構化數據存儲格式,平台無關、語言無關、可擴展,可用於通訊協議數據存儲等領域。

2. 為什么要用?
  - 平台無關,語言無關,可擴展;
  - 提供了友好的動態庫,使用簡單;
  - 解析速度快,比對應的XML快約20-100倍;
  - 序列化數據非常簡潔、緊湊,與XML相比,其序列化之后的數據量約為1/3到1/10。

3. 怎么安裝? 
  源碼下載地址: https://github.com/google/protobuf 
  安裝依賴的庫: autoconf automake libtool curl make g++ unzip  
安裝:

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

 

二、怎么用? 
1. 編寫proto文件 
  首先需要一個proto文件,其中定義了我們程序中需要處理的結構化數據:

 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 }

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文件:

 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("autyinjing@126.com");
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 } 

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. 輸出結果

 1 before clear(), id = 1219
 2 after  clear(), id = 0
 3 *****************************
 4 id:   1087
 5 name: aut
 6 email: autyinjing@126.com
 7 phone_type: 1
 8 phone_number: 021-8888-8888
 9 phone_type: 0
10 phone_number: 138-8888-8888
11 *****************************

 

三、怎么編碼的?

  protobuf之所以小且快,就是因為使用變長的編碼規則,只保存有用的信息,節省了大量空間。
1. Base-128變長編碼
  - 每個字節使用低7位表示數字,除了最后一個字節,其他字節的最高位都設置為1;
  - 采用Little-Endian字節序。

示例:

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

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
0 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進制表示)

 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" }

 

四、還有什么?

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM