從公司的項目源碼中看到了這個東西,覺得挺好用的,寫篇部落格做下小總結。下面的操作以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檔案,其中定義了我們程式中需要處理的結構化資料:
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: [email protected]
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 |
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