ProtoBuf
- Protocol Buffer ( protoBuf 或 PB )是 google 的一種數據交換的格式,它獨立於語言,獨立於平臺。
- google 提供了多種語言的實現:java、c#、c++、go 和 python,每一種實現都包含了相應語言的編譯器以及庫文件。
- 由於它是一種二進制的格式,比使用 XML 進行數據交換快許多,可以把它用於分佈式應用之間的數據通信或者異構環境下的數據交換,作爲一種效率和兼容性都很優秀的二進制數據傳輸格式,可以用於諸如網絡傳輸、配置文件、數據存儲等諸多領域。
優點
- 相比XML,它更小、更快、也更簡單。可以定義自己的數據結構,然後使用代碼生成器生成的代碼來讀寫這個數據結構,甚至可以在無需重新部署程序的情況下更新數據結構,只需使用 Protobuf 對數據結構進行一次描述,即可利用各種不同語言或從各種不同數據流中對你的結構化數據輕鬆讀寫。
- “向後”兼容性好。不必破壞已部署的、依靠“老”數據格式的程序就可以對數據結構進行升級,這樣程序就可以不必擔心因爲消息結構的改變而造成的大規模的代碼重構或者遷移的問題,因爲添加新的消息中的 field 並不會引起已經發布的程序的任何改變。
- Protobuf 語義更清晰。無需類似 XML 解析器的東西,Protobuf 編譯器就可將 .proto 文件編譯生成對應的數據訪問類以對 Protobuf 數據進行序列化、反序列化操作。
- 使用 Protobuf 無需學習複雜的文檔對象模型。Protobuf 的編程模式比較友好,簡單易學,同時它擁有良好的文檔和示例。
缺點
- 相比XML,它功能簡單,無法用來表示複雜的概念。
- XML 已經成爲多種行業標準的編寫工具,Protobuf 只是 Google 公司內部使用的工具,在通用性上還差很多。
- 由於文本並不適合用來描述數據結構,所以 Protobuf 也不適合用來對基於文本的標記文檔(如 HTML)建模。
- 由於 XML 具有某種程度上的自解釋性,它可以被人直接讀取編輯,在這一點上 Protobuf 不行,它以二進制的方式存儲,除非你有 .proto 定義,否則你沒法直接讀出 Protobuf 的任何內容。
用法
.proto 文件是 ProtoBuf 一個重要的文件,它定義了需要序列化數據的結構,用法步驟:
- 在.proto文件中定義消息格式
- 用 ProtoBuf 編譯器編譯 .proto 文件
- 用 C++ 對應的 ProtoBuf API 來寫或者讀消息
下載
下載地址:https://github.com/protocolbuffers/protobuf/releases
下載的安裝包:protoc-3.9.0-win32.zip、protoc-3.9.0-win64.zip
操作系統:WIN10 64位
下載的源碼包:protobuf-3.9.0.zip
安裝使用
1、解壓protoc-3.9.0-win32.zip到任意目錄
2、將解壓目錄 <path>\protoc-3.9.0-win32\bin 配置到環境變量path下
3、在命令行下即可使用 protoc 命令編譯 .proto 文件生成 C++ 對應的 .h 和 .cc 文件
在 cmd 命令行中進行:
> protoc --version #查看protoc的版本
#可以先 cd 到指定目錄,然後設置爲相對路徑
> protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/xxx.proto #編譯 .proto 文件
#-I:主要用於指定待編譯的 .proto 消息定義文件所在的目錄,即可能出現的包含文件的路徑,該選項可以被同時指定多個,此處指定的路徑不能爲空,如果是當前目錄,直接使用.,如果是子目錄,直接使用子目錄相對徑
編譯流程
操作系統:WIN10 64位
編譯工具:VS 2015
在 Win 上編譯需要用到 CMake :https://cmake.org/download/
Win 上編譯使用流程:
1、解壓源碼到任意路徑下 <path>\protobuf-3.9.0下
2、到官網(或國內下載站)下載 CMake,並安裝
3、CMake 生成 VS 2015 工程
- 打開CMake
- 設置源碼路徑下的cmake目錄 <path>/protobuf-3.9.0/cmake
- 設置任意構建目錄 <path>/probobuf_build
- 點 configure 按鈕
- 選擇對應的VS,這裏選的是VS 2015,選擇編譯爲WIN32,編譯器默認
- 點擊 Finish 按鈕,開始自動編譯(只要不報error表示順利,否則根據日誌查找原因)
- 點擊 Generate 按鈕生成VS項目
- 用VS打開生成的工程,根據需要選擇編譯libprotobuf、libprotobuf-lite、libprotoc和protoc項目
- 編譯完成
4、拷貝所需lib文件和protoc.exe到對應的項目中
5、 .proto文件編譯生成對應的.h和.cc文件
6、引入自己的工程使用
語法規則
1、編碼規範
- 描述文件以.proto做爲文件後綴。
- 結構定義包括:message、service、enum,這些以外的語句以分號結尾,rpc方法定義結尾的分號可有可無。
- message 命名採用駝峯命名方式,字段命名採用小寫字母加下劃線分隔方式。
- enums 類型名採用駝峯命名方式,字段命名採用大寫字母加下劃線分隔方式。
- service與rpc方法名統一採用駝峯式命名。
2、註釋
提供以下兩種註釋方式:
// 單行註釋
/* 多行註釋 */
3、默認值
解析消息時,如果編碼消息不包含特定的單數元素,則解析對象中的相應字段將設置爲該字段的默認值,這些默認值是特定於類型的。
- 對於字符串類型,默認值爲空字符串
- 對於字節類型,默認值爲空字節
- 對於布爾類型,默認值爲false
- 對於數字類型,默認值爲零
- 對於枚舉,默認值是第一個定義的枚舉值,該值必須爲0
- 對於消息字段,未設置該字段,它的確切值取決於語言
- 重複字段的默認值爲空(通常是相應語言的空列表)
4、message定義以及編譯生成 C++ 文件
- 在.proto文件定義消息,message是.proto文件最小的邏輯單元,由一系列name-value鍵值對構成。
package hw;
message test
{
required int32 id = 1;
required string str = 2;
optional int32 opt = 3;//可選字段
repeated string phone_num = 4;
}
- message消息包含一個或多個編號唯一的字段,每個字段由【字段限制+字段類型+字段名+編號】組成,字段限制分爲【optional(可選的)、required(必須的)、repeated(重複的)】
- required關鍵字 表示是一個必須字段,必須相對於發送方,在發送消息之前必須設置該字段的值,對於接收方,必須能夠識別該字段的意思。發送之前沒有設置required字段或者無法識別required字段都會引發編解碼異常,導致消息被丟棄。
- optional關鍵字 字面意思是可選的意思,具體protobuf裏面怎麼處理這個字段呢,就是protobuf處理的時候另外加了一個bool的變量,用來標記這個optional字段是否有值,發送方在發送的時候,如果這個字段有值,那麼就給bool變量標記爲true,否則就標記爲false,接收方在收到這個字段的同時,也會收到發送方同時發送的bool變量,拿着bool變量就知道這個字段是否有值了,可選對於發送方,在發送消息時,可以有選擇性的設置或者不設置該字段的值。對於接收方,如果能夠識別可選字段就進行相應的處理,如果無法識別,則忽略該字段,消息中的其它字段正常處理。optional字段的特性,很多接口在升級版本中都把後來添加的字段都統一的設置爲optional字段,這樣老的版本無需升級程序也可以正常的與新的軟件進行通信,只不過新的字段無法識別而已,因爲並不是每個節點都需要新的功能,因此可以做到按需升級和平滑過渡。
- repeated關鍵字 表示該字段可以包含0~N個元素。每一次可以包含多個值,可以看作是在傳遞一個數組的值。也是optional字段一樣,另外加了一個count計數變量,用於標明這個字段有多少個,這樣發送方發送的時候,同時發送了count計數變量和這個字段的起始地址,接收方在接受到數據之後,按照count來解析對應的數據即可。
- 定義好消息後,使用ProtoBuf編譯器生成C++對應的.h和.cc文件(hw.test.pb.h,hw.test.pb.cc),源文件提供了message消息的序列化和反序列化等方法,在生成的頭文件中定義了一個 C++ 類,該類提供了一系列操作定義結構的方法。
protoc -I=$SRC_DIR --cpp_out=$DST_DIR $SRC_DIR/hw.test.proto
5、嵌套以及導入message
import hw.test;//用 Import 關鍵字引入在其他 .proto 文件中定義的消息
package addresslist;
message Person
{
//1到15範圍內的字段編號需要一個字節進行編碼,包括字段編號和字段類型
//16到2047範圍內的字段編號佔用兩個字節
//應該爲非常頻繁出現的消息元素保留數字1到15
//要爲將來可能添加的常用元素留出一些空間
required string name = 1;
required int32 id = 2;
optional string email = 3;
enum PhoneType //定義枚舉的時候,我們要保證第一個枚舉值必須是0,枚舉值不能重複,
//除非使用 option allow_alias = true 選項來開啓別名
//枚舉值的範圍是32-bit integer,但因爲枚舉值使用變長編碼,
//所以不推薦使用負數作爲枚舉值,因爲這會帶來效率問題
{
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber //定義嵌套消息
{
required string number = 1;
optional PhoneType type = 2 [default = HOME];
}
repeated PhoneNumber phone = 4;
}
6、map映射
如果要在數據定義中創建關聯映射,Protocol Buffers提供了一種方便的語法:
map< key_type, value_type> map_field = N ;
其中key_type可以是任何整數或字符串類型。
- 枚舉不是有效的key_type,value_type可以是除map映射類型外的任何類型。
- map的字段可以是repeated。
- 序列化後的順序和map迭代器的順序是不確定的,所以你不要期望以固定順序處理map。
- 當爲.proto文件產生生成文本格式的時候,map會按照key 的順序排序,數值化的key會按照數值排序。
- 從序列化中解析或者融合時,如果有重複的key則後一個key不會被使用,當從文本格式中解析map時,如果存在重複的key,則解析可能會失敗。
- 如果爲映射字段提供鍵但沒有值,則字段序列化時的行爲取決於語言。
- 在Python中,使用類型的默認值。
7、oneof
如果你的消息中有很多可選字段, 並且同時至多一個字段會被設置, 你可以加強這個行爲,使用oneof特性節省內存。爲了在.proto定義oneof字段, 你需要在名字前面加上oneof關鍵字
message testMessage
{
oneof test_oneof
{
string name = 4;
SubMessage sub_message = 9;
}
}
然後你可以增加oneof字段到 oneof 定義中. 你可以增加任意類型的字段。
注意Oneof特性:
- oneof字段只有最後被設置的字段纔有效,即後面的set操作會覆蓋前面的set操作
- oneof不可以是repeated的
- 反射API可以作用於oneof字段
- 如果使用C++要防止內存泄露,即後面的set操作會覆蓋之前的set操作,導致前面設置的字段對象發生析構,要注意字段對象的指針操作
- 如果使用C++的Swap()方法交換兩條oneof消息,兩條消息都不會保存之前的字段
- 向後兼容。添加或刪除oneof字段的時候要注意,如果檢測到oneof字段的返回值是None/NOT_SET,這意味着oneof沒有被設置或者設置了一個不同版本的oneof的字段,但是沒有辦法能夠區分這兩種情況,因爲沒有辦法確認一個未知的字段是否是一個oneof的成員
- 編號複用問題。1)刪除或添加字段到oneof:在消息序列化或解析後會丟失一些信息,一些字段將被清空;2)刪除一個字段然後重新添加:在消息序列化或解析後會清除當前設置的oneof字段;3)分割或合併字段:同普通的刪除字段操作
8、Any(任意消息類型)
Any類型是一種不需要在.proto文件中定義就可以直接使用的消息類型,使用前import google/protobuf/any.proto文件即可。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
C++使用PackFrom()和UnpackTo()方法來打包和解包Any類型消息。
// Storing an arbitrary message type in Any.
NetworkErrorDetails details = ...;
ErrorStatus status;
status.add_details()->PackFrom(details);
// Reading an arbitrary message from Any.
ErrorStatus status = ...;
for (const Any& detail : status.details())
{
if (detail.Is<NetworkErrorDetails>())
{
NetworkErrorDetails network_error;
detail.UnpackTo(&network_error);
...
processing network_error
...
}
}
9、定義服務
如果想在RPC系統中使用消息類型,就需要在.proto文件中定義RPC服務接口,然後使用編譯器生成對應語言的存根。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
10、更新一個數據類型
在實際的開發中會存在這樣一種應用場景,既消息格式因爲某些需求的變化而不得不進行必要的升級,但是有些使用原有消息格式的應用程序暫時又不能被立刻升級,這便要求我們在升級消息格式時要遵守一定的規則,從而可以保證基於新老消息格式的新老程序同時運行。規則如下:
- 不要修改已經存在字段的標籤號。
- 任何新添加的字段必須是optional和repeated限定符,否則無法保證新老程序在互相傳遞消息時的消息兼容性。
- 在原有的消息中,不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標籤號必須被保留,不能被新的字段重用。
- int32、uint32、int64、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味着如果想修改原有字段的類型時,爲了保證兼容性,只能將其修改爲與其原有類型兼容的類型,否則就將打破新老消息格式的兼容性。
- optional和repeated限定符也是相互兼容的。
在 C++ 中的示例
1、定義.proto文件
syntax = "proto2";//不定義默認是這個(有警告),也可以定義成"proto3"
option java_package = "com.demo.myproto";
//使用精簡版的protobuf庫,需要鏈接libprotobuf-lite.lib
option optimize_for = LITE_RUNTIME;//生成的可執行程序速度快,體積小,以犧牲Protocol Buffer提供的反射功能爲代價
package myproto;
message HelloWorld {
required int32 id = 1;
required string str = 2;
optional int32 opt=3;
}
保存成myproto.proto文件,放到指定目錄
2、生成 C++ 文件
打開 cmd 命令行,cd 到指定目錄下
> protoc -I ./ --cpp_out=./ myproto.proto #在本目錄下轉
3、將生成的 C++ 文件加入工程中
4、附加protobuf頭文件目錄(protobuf源文件 src 下的 google 文件夾引入到工程,生成的 C++ 文件需要)、附加依賴庫文件目錄、附加依賴庫文件名 libprotobuf-lite.lib、設置運行庫一致
5、編寫測試程序
#include <stdlib.h>
#include <stdio.h>
#include "myproto.pb.h"
#include <google/protobuf/io/zero_copy_stream_impl_lite.h>
#include <google/protobuf/message_lite.h>
/*
message HelloWorld {
required int32 id = 1;
required string str = 2;
optional int32 opt=3;
}
*/
int main()
{
printf("My Hello World Application !\r\n");
//設置參數
myproto::HelloWorld in_msg;
in_msg.set_id(1);
in_msg.set_str("zab");
in_msg.set_opt(0);
//對消息進行編碼
int nSize = in_msg.ByteSize() + 8;
char *pcData = new char[nSize];
memset(pcData, 0, nSize);
int nEncodedLen = 0;
google::protobuf::io::ArrayOutputStream array_stream(pcData, nSize);
google::protobuf::io::CodedOutputStream output_stream(&array_stream);//操作編碼時的varints
//Varint 是一種緊湊的表示數字的方法
//它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數
//這能減少用來表示數字的字節數
//Varint 中的每個 byte 的最高位 bit 有特殊的含義,如果該位爲 1,表示後續的 byte 也是該數字的一部分,如果該位爲 0,則結束
//其他的 7 個 bit 都用來表示數字
//因此小於 128 的數字都可以用一個 byte 表示
//大於 128 的數字,會用兩個字節
output_stream.WriteVarint32(in_msg.ByteSize());//寫爲Varint
if (in_msg.SerializeToCodedStream(&output_stream)) {
nEncodedLen = output_stream.ByteCount();
//對消息進行解碼
myproto::HelloWorld out_msg;
//構造一個InputStream,返回data所指向的字節數組
//若指定了block_size,則每次調用Next()返回不超過指定大小的數據塊;否則,返回整個字節數組
google::protobuf::io::CodedInputStream input_stream ((google::protobuf::uint8*)pcData, nEncodedLen);//操作解碼時的varints
google::protobuf::uint32 ui_size = 0;
if (input_stream.ReadVarint32(&ui_size)) {//還原Varint
google::protobuf::io::CodedInputStream::Limit limit = input_stream.PushLimit(ui_size);//保護在長度範圍內解析
if (out_msg.MergeFromCodedStream(&input_stream)) {//分割填充
// 當對報文進行解析後,以進一步判斷報文是否被以正確方式讀取完畢
if (input_stream.ConsumedEntireMessage()) {
input_stream.PopLimit(limit);//釋放保護
printf("HelloWorld id=%d,str=%s,opt=%d.\r\n", out_msg.id(), out_msg.str().c_str(), out_msg.opt());
} else {
printf("consume msg fail!\r\n");
}
} else {
printf("merge msg stream fail!\r\n");
}
} else {
printf("read msg stream fail!\r\n");
}
} else {
printf("encode msg fail!\r\n");
}
if (NULL != pcData) {
delete[] pcData;
pcData = NULL;
}
nEncodedLen = 0;
printf("My Hello World Application End!\r\n");
system("pause");
return 0;
}
ProtoBuf的三種優化級別
optimize_for是文件級別的選項,Protocol Buffer 定義三種優化級別SPEED、CODE_SIZE、LITE_RUNTIME,缺省情況下是SPEED。
- SPEED: 表示生成的代碼運行效率高,但是由此生成的代碼編譯後會佔用更多的空間
- CODE_SIZE: 和SPEED恰恰相反,代碼運行效率較低,但是由此生成的代碼編譯後會佔用更少的空間,通常用於資源有限的平臺,如Mobile
- LITE_RUNTIME: 生成的代碼執行效率高,同時生成代碼編譯後的所佔用的空間也是非常少,以犧牲Protocol Buffer提供的反射功能爲代價的,在C++中鏈接Protocol Buffer庫時僅需鏈接libprotobuf-lite,而非libprotobuf,在Java中僅需包含protobuf-java-2.4.1-lite.jar,而非protobuf-java-2.4.1.jar
- SPEED和LITE_RUNTIME相比,在於調試級別上,例如 msg.SerializeToString(&str) 在SPEED模式下會利用反射機制打印出詳細字段和字段值,但是LITE_RUNTIME則僅僅打印字段值組成的字符串,因此可以在程序調試階段使用 SPEED模式,而上線以後使用提升性能使用 LITE_RUNTIME 模式優化
動態編譯
動態編譯
一般情況下,使用 Protobuf 的人們都會先寫好 .proto 文件,再用 Protobuf 編譯器生成目標語言所需要的源代碼文件,可是在某且情況下,人們無法預先知道 .proto 文件,他們需要動態處理一些未知的 .proto 文件,比如一個通用的消息轉發中間件,它不可能預知需要處理怎樣的消息,這需要動態編譯 .proto 文件,並使用其中的 Message。
無須編譯.proto生成.pb.cc和.pb.h,需要使用protobuf中提供的兩個頭文件
- protobuf中頭文件的位置/usr/local/include/google/protobuf
- google/protobuf/compiler/importer.h ,google/protobuf/dynamic_message.h
示例
#include <iostream>
#include <string>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
class MyErrorCollector: public google::protobuf::compiler::MultiFileErrorCollector
{
virtual void AddError(const std::string & filename, int line, int column, const std::string & message){
// define import error collector
printf("%s, %d, %d, %s\n", filename.c_str(), line, column, message.c_str());
}
};
int main()
{
google::protobuf::compiler::DiskSourceTree sourceTree; // source tree
sourceTree.MapPath("", "/home/szw/code/protobuf/tmp2"); // initialize source tree
MyErrorCollector errorCollector; // dynamic import error collector
google::protobuf::compiler::Importer importer(&sourceTree, &errorCollector); // importer
importer.Import("test.proto");
// find a message descriptor from message descriptor pool
const google::protobuf::Descriptor * descriptor = importer.pool()->FindMessageTypeByName("lm.helloworld");
if (!descriptor){
perror("Message is not found!");
exit(-1);
}
// create a dynamic message factory
google::protobuf::DynamicMessageFactory * factory = new google::protobuf::DynamicMessageFactory(importer.pool());
// create a const message ptr by factory method and message descriptor
const google::protobuf::Message * tmp = factory->GetPrototype(descriptor);
// create a non-const message object by const message ptr
// define import error collector
google::protobuf::Message * msg = tmp->New();
return 0;
}
其中,頭文件<google/protobuf/compiler/importer.h>包含了編譯器對象相關,頭文件<google/protobuf/dynamic_message.h>包含了Message_Descriptor和Factory相關
- 首先初始化一個DiskSourceTree對象, 指明目標.proto文件的根目錄
- 創建一個MyErrorCollector對象, 用於收集動態編譯過程中產生的編譯bug, 該對象需要根據proto提供的純虛基類派生, 並需要重寫其中的純虛函數, 使之不再是一個抽象類
- 初始化一個Importer對象, 用於動態編譯, 將DiskSourceTree和MyErrorCollector的地址傳入
- 將目標.proto動態編譯, 並加入編譯器池中
- 可以通過FindMessageTypeName()和消息名, 來獲取到目標消息的Message_Descriptor
- 創建動態工廠, 並利用工廠方法GetPrototype和目標消息的Message_Descriptor獲取一個指針const message *
- 利用消息實例指針的New()方法獲取到non-const message ptr
類型反射
類型反射即是根據字段的名稱獲取到字段類型的一種機制,protobuf自身便配備了類型反射機制,需要頭文件<google/protobuf/descriptor.h>與<google/protobuf/message.h>。
protobuf對於每個元素都有一個相應的descriptor,這個descriptor包含該元素的所有元信息。
- FileDescriptor: 對一個proto文件的描述,它包含文件名、包名、選項(如package, java_package, java_outer_classname等)、文件中定義的所有message、文件中定義的所有enum、文件中定義的所有service、文件中所有定義的extension、文件中定義的所有依賴文件(import)等。在FileDescriptor中還存在一個DescriptorPool實例,它保存了所有的dependencies(依賴文件的FileDescriptor)、name到GenericDescriptor的映射、字段到FieldDescriptor的映射、枚舉項到EnumValueDescriptor的映射,從而可以從該DescriptorPool中查找相關的信息,因而可以通過名字從FileDescriptor中查找Message、Enum、Service、Extensions等。可以通過 --descriptor_set_out 指定生成某個proto文件相對應的FileDescriptorSet文件。
- Descriptor: 對一個message定義的描述,它包含該message定義的名字、所有字段、內嵌message、內嵌enum、關聯的FileDescriptor等。可以使用字段名或字段號查找FieldDescriptor。
- FieldDescriptor:對一個字段或擴展字段定義的描述,它包含字段名、字段號、字段類型、字段定義(required/optional/repeated/packed)、默認值、是否是擴展字段以及和它關聯的Descriptor/FileDescriptor等。
- EnumDescriptor:對一個enum定義的描述,它包含enum名、全名、和它關聯的FileDescriptor。可以使用枚舉項或枚舉值查找EnumValueDescriptor
- EnumValueDescriptor:對一個枚舉項定義的描述,它包含枚舉名、枚舉值、關聯的EnumDescriptor/FileDescriptor等。
- ServiceDescriptor:對一個service定義的描述,它包含service名、全名、關聯的FileDescriptor等。
- MethodDescriptor:對一個在service中的method的描述,它包含method名、全名、參數類型、返回類型、關聯的FileDescriptor/ServiceDescriptor等。
動態定義proto
能不能通過程序生成protobuf文件呢?答案是可以的。FileDescriptorProto允許你動態的定義你的proto文件:
//syntax = "proto3";
//message mymsg
//{
// uint32 len = 1;
// uint32 type = 2;
//}
FileDescriptorProto file_proto;
file_proto.set_name("my.proto");
file_proto.set_syntax("proto3");
DescriptorProto *message_proto = file_proto.add_message_type();
message_proto->set_name("mymsg");
FieldDescriptorProto *field_proto = NULL;
field_proto = message_proto->add_field();
field_proto->set_name("len");
field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);
field_proto->set_number(1);
field_proto->set_label(FieldDescriptorProto::LABEL_OPTIONAL);
field_proto = message_proto->add_field();
field_proto->set_name("type");
field_proto->set_type(FieldDescriptorProto::TYPE_UINT32);
field_proto->set_number(2);
DescriptorPool pool;
const FileDescriptor *file_descriptor = pool.BuildFile(file_proto);
cout << file_descriptor->DebugString();
Protobuf 序列化原理
Varint
Varint 是一種緊湊的表示數字的方法。它用一個或多個字節來表示一個數字,值越小的數字使用越少的字節數。這能減少用來表示數字的字節數。
比如對於 int32 類型的數字,一般需要 4 個 byte 來表示。但是採用 Varint,對於很小的 int32 類型的數字,則可以用 1 個 byte 來表示。當然凡事都有好的也有不好的一面,採用 Varint 表示法,大的數字則需要 5 個 byte 來表示。從統計的角度來說,一般不會所有的消息中的數字都是大數,因此大多數情況下,採用 Varint 後,可以用更少的字節數來表示數字信息。下面就詳細介紹一下 Varint。
Varint 中的每個 byte 的最高位 bit 有特殊的含義,如果該位爲 1,表示後續的 byte 也是該數字的一部分,如果該位爲 0,則結束。其他的 7 個 bit 都用來表示數字。因此小於 128 的數字都可以用一個 byte 表示。大於 128 的數字,比如 300,會用兩個字節來表示:1010 1100 0000 0010
下圖演示了 Google Protocol Buffer 如何解析兩個 bytes。注意到最終計算前將兩個 byte 的位置相互交換過一次,這是因爲 Google Protocol Buffer 字節序採用 little-endian 的方式。
消息經過序列化後會成爲一個二進制數據流,該流中的數據爲一系列的 Key-Value 對。如下圖所示:
採用這種 Key-Pair 結構無需使用分隔符來分割不同的 Field。對於可選的 Field,如果消息中不存在該 field,那麼在最終的 Message Buffer 中就沒有該 field,這些特性都有助於節約消息本身的大小。
時間效率
通過protobuf序列化/反序列化的過程可以得出:protobuf是通過算法生成二進制流,序列化與反序列化不需要解析相應的節點屬性和多餘的描述信息,所以序列化和反序列化時間效率較高。
空間效率
xml、json是用字段名稱來確定類實例中字段之間的獨立性,所以序列化後的數據多了很多描述信息,增加了序列化後的字節序列的容量。Protobuf的序列化/反序列化過程可以得出:protobuf是由字段索引(fieldIndex)與數據類型(type)計算(fieldIndex<<3|type)得出的key維護字段之間的映射且只佔一個字節,所以相比json與xml文件,protobuf的序列化字節沒有過多的key與描述符信息,所以佔用空間要小很多。
參考:
https://www.ibm.com/developerworks/cn/linux/l-cn-gpb/