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

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

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

2. 爲何要用?
  - 平臺無關,語言無關,可擴展;
  - 提供了友好的動態庫,使用簡單;
  - 解析速度快,比對應的XML快約20-100倍;
  - 序列化數據很是簡潔、緊湊,與XML相比,其序列化以後的數據量約爲1/3到1/10。ios

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

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

 

2、怎麼用? 
1. 編寫proto文件 
  首先須要一個proto文件,其中定義了咱們程序中須要處理的結構化數據:github

 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,在編譯的時候他們會被編譯成爲不一樣的類。
curl

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++文件。ide

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 *****************************

 

3、怎麼編碼的?

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

 

4、還有什麼?

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

相關文章
相關標籤/搜索