本文是對官方文檔的翻譯,然后截取了一篇非常優秀的文章片段來幫助理解,本人英文水平有限,基本都是直譯,如果有不理解的地方請參考英文官方文檔,參考的文章鏈接在文章末尾
protocol buffers簡介
protocol buffer是google的一個開源項目,它是用於結構化數據串行化的靈活、高效、自動的方法,例如XML,不過它比xml更小、更快、也更簡單。你可以定義自己的數據結構,然后使用代碼生成器生成的代碼來讀寫這個數據結構。你甚至可以在無需重新部署程序的情況下更新數據結構
protocol buffers是如何工作的
在.proto
文件定義消息,message是.proto
文件最小的邏輯單元,由一系列name-value鍵值對構成。下面的.proto
文件定義了一個"人"的消息:
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 phone = 4;
}
message消息包含一個或多個編號唯一的字段,每個字段由字段限制,字段類型,字段名和編號四部分組成,字段限制分為:optional(可選的)、required(必須的)以及repeated(重復的)。定義好消息后,使用ProtoBuf編譯器生成C++對應的.h
和.cc
文件,源文件提供了message消息的序列化和反序列化等方法:
# 序列化數據
Person person;
person.set_name("John Doe");
person.set_id(1234);
person.set_email("jdoe@example.com");
fstream output("myfile", ios::out | ios::binary);
person.SerializeToOstream(&output);
# 反序列化數據
fstream input("myfile", ios::in | ios::binary);
Person person;
person.ParseFromIstream(&input);cout << "Name: " << person.name() << endl;cout << "E-mail: " << person.email() << endl;
為什么不直接使用XML
同XML相比,Protobuf的優勢在於高性能,它以高效的二進制存儲方式比XML小3到10倍,快20到100倍,原因在於:
- ProtoBuf序列化后所生成的二進制消息非常緊湊
- ProtoBuf封解包過程非常簡單
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的數字會用兩個字節來表示。
Varint編解碼
比如數值300
用Varint來表示就是:1010 1100 0000 0010。下圖演示了Google Protocol Buffer解析Varint表示的300
的過程,由於Google Protocol Buffer采用小端字節序,所以實際存儲的字節順序是反過來的:
Google Protocol Buffer序列化
消息經過序列化后會成為一個二進制數據流,該流中的數據為一系列的Key-Value對。如下圖所示:
采用這種Key-Pair結構無需使用分隔符來分割不同的 Field。對於可選的Field,如果消息中不存在該Field,那么在最終的Message Buffer中就沒有該Field,這些特性都有助於節約消息本身的大小。Key 用來標識具體的Field,在解包的時候ProtoBuf根據Key就可以知道相應的Value應該對應於消息中的哪一個Field。Key由字段的編號和字段的線性傳輸類型構成(field_number << 3) | wire_type
wire_type | Meaning | Used For |
---|---|---|
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum |
1 | 64-bit | fixed64, sfixed64, double |
2 | Length-delimi | string, bytes, embedded messages, packed repeated fields |
3 | Start group | Groups (deprecated) |
4 | End group | Groups (deprecated) |
5 | 32-bit | fixed32, sfixed32, float |
- Google Protocol Buffer采用zigzag編碼來用無符號數來表示有符號數字,zigzag采用正數和負數交錯的方式來同時表示無符號數來表示有符號數字,如圖所示:
使用zigzag編碼,絕對值小的數字,無論正負都可以采用較少的byte來表示,充分利用了Varint這種技術。
- 其他的數據類型,比如字符串等則采用類似數據庫中的varchar的表示方法,即用一個varint表示長度,然后將其余部分緊跟在這個長度部分之后即可。
ProtoBuf編碼與XML編碼對比
消息定義如下:
package lm;
message helloworld
{
required int32 id = 1; // ID
required string str = 2; // str
optional int32 opt = 3; //optional field
}
假設有一條helloworld
消息id=101 str="hello"
,那么用Protobuf序列化后的字節序列為:
08 65 12 06 48 65 6C 6C 6F 77
而如果用XML,則類似這樣:
31 30 31 3C 2F 69 64 3E 3C 6E 61 6D 65 3E 68 65
6C 6C 6F 3C 2F 6E 61 6D 65 3E 3C 2F 68 65 6C 6C
6F 77 6F 72 6C 64 3E
一共 55 個字節,這些奇怪的數字需要稍微解釋一下,其含義用 ASCII 表示如下:
<helloworld>
<id>101</id>
<name>hello</name>
</helloworld></pre>
ProtoBuf封解包
首先我們來了解一下XML的封解包過程。XML需要從文件中讀取出字符串,再轉換為XML文檔對象結構模型。之后再從XML文檔對象結構模型中讀取指定節點的字符串,最后再將這個字符串轉換成指定類型的變量,這個過程非常復雜。其中將XML文件轉換為文檔對象結構模型的過程通常需要完成詞法文法分析等大量消耗 CPU 的復雜計算。
反觀Protobuf,它只需要簡單地將一個二進制序列按照指定的格式讀取到C++對應的結構類型中就可以了。從上一節的描述可以看到,消息的解碼過程也可以通過幾個位移操作組成的表達式計算即可完成,速度非常快。
上面例子中,Protobuf解包helloworld消息的過程可以用下圖表示:
整個解析過程需要Protobuf本身的框架代碼和由Protobuf編譯器生成的代碼共同完成。其中Message以及Message_lite作為通用的流程框架,CodedInputStream、WireFormatLite提供了對二進制數據的解碼功能,而且Protobuf的解碼可以通過幾個簡單的數學運算完成,無需復雜的詞法語法分析,因此圖中ReadTag()等方法都非常快。相對於XML的解析,整個調用路徑上的其他類和方法都非常簡單,這也就是ProtoBuf封解包速度迅速的原因。