轉自:https://www.jianshu.com/p/419efe983cb2
protobuf是google團隊開發的用於高效存儲和讀取結構化數據的工具。什么是結構化數據呢,正如字面上表達的,就是帶有一定結構的數據。比如電話簿上有很多記錄數據,每條記錄包含姓名、ID、郵件、電話等,這種結構重復出現。
xml、json也可以用來存儲此類結構化數據,但是使用protobuf表示的數據能更加高效,並且將數據壓縮得更小,大約是json格式的1/10,xml格式的1/20。
下面介紹的內容基於protobuf 2.6版本。
1.定義message結構
protobuf將一種結構稱為一個message類型,我們以電話簿中的數據為例。
message Person { required string name = 1; required int32 id = 2; [default = 0] optional string email = 3; repeated int32 samples = 4 [packed=true]; }
其中Person是message這種結構的名稱,name、id、email是其中的Field,每個Field保存着一種數據類型,后面的1、2、3是Filed對應的數字id。id在115之間編碼只需要占一個字節,包括Filed數據類型和Filed對應數字id,在162047之間編碼需要占兩個字節,所以最常用的數據對應id要盡量小一些。后面具體講到編碼規則時會細講。
Field最前面的required,optional,repeated是這個Filed的規則,分別表示該數據結構中這個Filed有且只有1個,可以是0個或1個,可以是0個或任意個。optional后面可以加default默認值,如果不加,數據類型的默認為0,字符串類型的默認為空串。repeated后面加[packed=true]會使用新的更高效的編碼方式。
注意:使用required規則的時候要謹慎,因為以后結構若發生更改,這個Filed若被刪除的話將可能導致兼容性的問題。
保留Filed和保留Filed number
每個Filed對應唯一的數字id,但是如果該結構在之后的版本中某個Filed刪除了,為了保持向前兼容性,需要將一些id或名稱設置為保留的,即不能被用來定義新的Field。
message Person {
reserved 2, 15, 9 to 11;
reserved "samples", "email";
}
枚舉類型
比如電話號碼,只有移動電話、家庭電話、工作電話三種,因此枚舉作為選項,如果沒設置的話枚舉類型的默認值為第一項。在上面的例子中在個人message中加入電話號碼這個Filed。如果枚舉類型中有不同的名字對應相同的數字id,需要加入option allow_alias = true這一項,否則會報錯。枚舉類型中也有reserverd Filed和number,定義和message中一樣。
message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { //allow_alias = true; MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phones = 4; }
引用其它message類
在同一個文件中,可以直接引用定義過的message類型。
在同一個項目中,可以用import來導入其它message類型。
import "myproject/other_protos.proto";
或者在一個message類型中嵌套定義其它的message類型。
message擴展
message Person { // ... extensions 100 to 199; }
在另一個文件中,import 這個proto之后,可以對Person這個message進行擴展。
extend Person { optional int32 bar = 126; }
2.數據類型對應關系
在使用規則創建proto類型的數據結構文件之后,會將其轉化成對應編程語言中的頭文件或者類定義。
proto中的數據類型和c++,Python中的數據類型對應規則如下:
| .proto | C++ | Python | 介紹 |
|---|---|---|---|
| double | double | float | |
| float | float | float | |
| int32 | int32 | int | 可變長編碼,對負數效率不高 |
| int64 | int64 | int/long | |
| uint32 | uint32 | int/long | |
| uint64 | uint64 | int/long | |
| sint32 | int32 | int | 可變長編碼,對負數效率較高 |
| sint64 | int64 | int/long | |
| fixed32 | uint32 | int/long | 32位定長編碼 |
| fixed64 | uint64 | int/long | |
| sfixed32 | int32 | int | |
| sfixed64 | int64 | int/long | |
| bool | bool | bool | |
| string | string | str/unicode | UTF-8編碼或者7-ASCII編碼 |
| bytes | string | str |
3.編碼規則
protobuf有一套高效的數據編碼規則。
可變長整數編碼
每個字節有8bits,其中第一個bit是most significant bit(msb),0表示結束,1表示還要讀接下來的字節。
對message中每個Filed來說,需要編碼它的數據類型、對應id以及具體數據。
數據類型有以下6種,可以用3個bits表示。每個整數編碼用最后3個bits表示數據類型。所以,對應id在1~15之間的Filed,可以用1個字節編碼數據類型、對應id。
| 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, packed repeated | fields |
| 3 | Start group | groups (deprecated) | |
| 4 | End group | groups (deprecated) | |
| 5 | 32-bit | fixed32, sfixed32, float |
比如對於下面這個例子來說,如果給a賦值150,那么最終得到的編碼是什么呢?
message Test { optional int32 a = 1; }
首先數據類型編碼是000,因此和id聯合起來的編碼是00001000. 然后值150的編碼是1 0010110,采用小端序交換位置,即0010110 0000001,前面補1后面補0,即10010110 00000001,即96 01,加上最前面的數據類型編碼字節,總的編碼為08 96 01。
有符號整數編碼
如果用int32來保存一個負數,結果總是有10個字節長度,被看做是一個非常大的無符號整數。使用有符號類型會更高效。它使用一種ZigZag的方式進行編碼。即-1編碼成1,1編碼成2,-2編碼成3這種形式。
也就是說,對於sint32來說,n編碼成 (n << 1) ^ (n >> 31),注意到第二個移位是算法移位。
定長編碼
定長編碼是比較簡單的情況。
4.安裝protobuf包
這里在Mac上下載protobuf 2.6版本記性測試。
$ wget https://github.com/protocolbuffers/protobuf/releases/download/v2.6.1/protobuf-2.6.1.tar.gz $ tar -xvf protobuf-2.6.1.tar.gz $ cd protobuf-2.6.1 $ ./configure $ make -j8
5.Python測試代碼
1.創建一個addressbook.proto文件如下:
syntax = "proto2"; package tutorial; message Person { required string name = 1; required int32 id = 2; optional string email = 3; enum PhoneType { MOBILE = 0; HOME = 1; WORK = 2; } message PhoneNumber { required string number = 1; optional PhoneType type = 2 [default = HOME]; } repeated PhoneNumber phones = 4; } message AddressBook { repeated Person people = 1; }
2.找到src/protoc工具,命令行執行
protoc -I=$SRC_DIR --python_out=$DST_DIR $SRC_DIR/addressbook.proto
運行完該命令會生成addressbook_pb2.py文件。
3.protobuf的python安裝
$ cd protobuf-2.6.1/python $ python setup.py install # 如果出現報錯package directory 'google/protobuf/compiler' does not exist,則 $ mkdir google/protobuf/compiler
4.python下基本用法
# encoding:utf-8 import sys import addressbook_pb2 # 獲取類型 address_book = addressbook_pb2.AddressBook() # 添加數據 person = address_book.people.add() # 添加值 person.id = 1234 person.name = "John Doe" person.email = "jdoe@example.com" phone = person.phones.add() phone.number = "555-4321" # enum的數據引用 phone.type = addressbook_pb2.Person.HOME # 檢查是否所有required的Filed都有賦值 print(person.IsInitialized()) # 序列化 res = person.SerializeToString() # 反序列化 a = addressbook_pb2.Person() a.ParseFromString(res) # 從其它message載入,會覆蓋當前的值 b = addressbook_pb2.Person() b.name = "Tom" b.CopyFrom(a) # 清除所有的Filed a.Clear() # 打印出來 print(b)
6.C++測試代碼
1.同上創建一個addressbook.proto文件。
2.找到src/protoc工具,命令行執行
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
運行完該命令會生成addressbook.pb.h,addressbook.pb.cc文件。
3.protobuf的c++環境安裝
cd protobuf-2.6.1
sudo make install
4.c++下基本用法
參考文獻
[1] https://developers.google.com/protocol-buffers/docs/cpptutorial
