Protobuf學習 - 入門

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("[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 } 
複製代碼

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. 字段編碼類型

TypeMeaningUsed For
0Varintint32, int64, uint32, uint64, sint32, sint64, bool, enum
164-bitfixed64, sfixed64, double
2Length-delimitedstring, bytes, embedded messages(嵌套message), packed repeated fields
3Start groupgroups (廢棄) 
4End groupgroups (廢棄)
532-bitfixed32, 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 122 << 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 122 << 3 | 2
62 03: strlen("aut") 
63 61 75 74 -> a u t
64 122 << 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 122 << 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++類型備註
doubledouble 
floatfloat 
int32int32使用可變長編碼,編碼負數時不夠高效——如果字段可能含有負數,請使用sint32
int64int64使用可變長編碼,編碼負數時不夠高效——如果字段可能含有負數,請使用sint64
uint32uint32使用可變長編碼
uint64uint64使用可變長編碼
sint32int32使用可變長編碼,有符號的整型值,編碼時比通常的int32高效
sint64int64使用可變長編碼,有符號的整型值,編碼時比通常的int64高效
fixed32uint32總是4個字節,如果數值總是比總是比228大的話,這個類型會比uint32高效
fixed64uint64總是8個字節,如果數值總是比總是比256大的話,這個類型會比uint64高效
sfixed32int32總是4個字節
sfixed64int64總是8個字節
boolbool 
stringstring一個字符串必須是UTF-8編碼或者7-bit ASCII編碼的文本
bytesstring可能包含任意順序的字節數據


 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  

參考資料 
1. Protocol Buffers Developer Guide

2. Google Protocol Buffer 的使用和原理

3. 淺談幾種序列化協議

4. 序列化和反序列化

5. Protobuf使用手冊

6.Google protobuf 簡介

(本文完)

發佈了7 篇原創文章 · 獲贊 21 · 訪問量 7萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章