從公司的項目源碼中看到了這個東西,以爲挺好用的,寫篇博客作下小總結。下面的操做以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