protobuf是google團隊開發(fā)的用于高效存儲和讀取結(jié)構(gòu)化數(shù)據(jù)的工具。什么是結(jié)構(gòu)化數(shù)據(jù)呢,正如字面上表達的,就是帶有一定結(jié)構(gòu)的數(shù)據(jù)。比如電話簿上有很多記錄數(shù)據(jù),每條記錄包含姓名、ID、郵件、電話等,這種結(jié)構(gòu)重復(fù)出現(xiàn)。
xml、json也可以用來存儲此類結(jié)構(gòu)化數(shù)據(jù),但是使用protobuf表示的數(shù)據(jù)能更加高效,并且將數(shù)據(jù)壓縮得更小,大約是json格式的1/10,xml格式的1/20。
下面介紹的內(nèi)容基于protobuf 2.6版本。
1.定義message結(jié)構(gòu)
protobuf將一種結(jié)構(gòu)稱為一個message類型,我們以電話簿中的數(shù)據(jù)為例。
message Person {
required string name = 1;
required int32 id = 2; [default = 0]
optional string email = 3;
repeated int32 samples = 4 [packed=true];
}
其中Person是message這種結(jié)構(gòu)的名稱,name、id、email是其中的Field,每個Field保存著一種數(shù)據(jù)類型,后面的1、2、3是Filed對應(yīng)的數(shù)字id。id在115之間編碼只需要占一個字節(jié),包括Filed數(shù)據(jù)類型和Filed對應(yīng)數(shù)字id,在162047之間編碼需要占兩個字節(jié),所以最常用的數(shù)據(jù)對應(yīng)id要盡量小一些。后面具體講到編碼規(guī)則時會細講。
Field最前面的required,optional,repeated是這個Filed的規(guī)則,分別表示該數(shù)據(jù)結(jié)構(gòu)中這個Filed有且只有1個,可以是0個或1個,可以是0個或任意個。optional后面可以加default默認值,如果不加,數(shù)據(jù)類型的默認為0,字符串類型的默認為空串。repeated后面加[packed=true]會使用新的更高效的編碼方式。
注意:使用required規(guī)則的時候要謹慎,因為以后結(jié)構(gòu)若發(fā)生更改,這個Filed若被刪除的話將可能導(dǎo)致兼容性的問題。
保留Filed和保留Filed number
每個Filed對應(yīng)唯一的數(shù)字id,但是如果該結(jié)構(gòu)在之后的版本中某個Filed刪除了,為了保持向前兼容性,需要將一些id或名稱設(shè)置為保留的,即不能被用來定義新的Field。
message Person {
reserved 2, 15, 9 to 11;
reserved "samples", "email";
}
枚舉類型
比如電話號碼,只有移動電話、家庭電話、工作電話三種,因此枚舉作為選項,如果沒設(shè)置的話枚舉類型的默認值為第一項。在上面的例子中在個人message中加入電話號碼這個Filed。如果枚舉類型中有不同的名字對應(yīng)相同的數(shù)字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來導(dǎo)入其它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.數(shù)據(jù)類型對應(yīng)關(guān)系
在使用規(guī)則創(chuàng)建proto類型的數(shù)據(jù)結(jié)構(gòu)文件之后,會將其轉(zhuǎn)化成對應(yīng)編程語言中的頭文件或者類定義。
proto中的數(shù)據(jù)類型和c++,Python中的數(shù)據(jù)類型對應(yīng)規(guī)則如下:
| .proto | C++ | Python | 介紹 |
|---|---|---|---|
| double | double | float | |
| float | float | float | |
| int32 | int32 | int | 可變長編碼,對負數(shù)效率不高 |
| int64 | int64 | int/long | |
| uint32 | uint32 | int/long | |
| uint64 | uint64 | int/long | |
| sint32 | int32 | int | 可變長編碼,對負數(shù)效率較高 |
| 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.編碼規(guī)則
protobuf有一套高效的數(shù)據(jù)編碼規(guī)則。
可變長整數(shù)編碼
每個字節(jié)有8bits,其中第一個bit是most significant bit(msb),0表示結(jié)束,1表示還要讀接下來的字節(jié)。
對message中每個Filed來說,需要編碼它的數(shù)據(jù)類型、對應(yīng)id以及具體數(shù)據(jù)。
數(shù)據(jù)類型有以下6種,可以用3個bits表示。每個整數(shù)編碼用最后3個bits表示數(shù)據(jù)類型。所以,對應(yīng)id在1~15之間的Filed,可以用1個字節(jié)編碼數(shù)據(jù)類型、對應(yīng)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;
}
首先數(shù)據(jù)類型編碼是000,因此和id聯(lián)合起來的編碼是00001000. 然后值150的編碼是1 0010110,采用小端序交換位置,即0010110 0000001,前面補1后面補0,即10010110 00000001,即96 01,加上最前面的數(shù)據(jù)類型編碼字節(jié),總的編碼為08 96 01。
有符號整數(shù)編碼
如果用int32來保存一個負數(shù),結(jié)果總是有10個字節(jié)長度,被看做是一個非常大的無符號整數(shù)。使用有符號類型會更高效。它使用一種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.創(chuàng)建一個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工具,命令行執(zhí)行
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
# 如果出現(xiàn)報錯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()
# 添加數(shù)據(jù)
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的數(shù)據(jù)引用
phone.type = addressbook_pb2.Person.HOME
# 檢查是否所有required的Filed都有賦值
print(person.IsInitialized())
# 序列化
res = person.SerializeToString()
# 反序列化
a = addressbook_pb2.Person()
a.ParseFromString(res)
# 從其它message載入,會覆蓋當(dāng)前的值
b = addressbook_pb2.Person()
b.name = "Tom"
b.CopyFrom(a)
# 清除所有的Filed
a.Clear()
# 打印出來
print(b)
6.C++測試代碼
1.同上創(chuàng)建一個addressbook.proto文件。
2.找到src/protoc工具,命令行執(zhí)行
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/addressbook.proto
運行完該命令會生成addressbook.pb.h,addressbook.pb.cc文件。
3.protobuf的c++環(huán)境安裝
cd protobuf-2.6.1
sudo make install
4.c++下基本用法
參考文獻
[1] https://developers.google.com/protocol-buffers/docs/cpptutorial