定義:
一種 結構化數據 的數據存儲格式。(類似於xml, json)
作用:
通過將 結構化的數據 進行串行化(序列化),從而實現 數據存儲/rpc數據交換 的功能
序列化:將數據結構或對象 轉換成 二進制的 過程
飯序列化:將在序列化過程中所生成的二進制串 轉換成 數據結構或對象 的過程。
特點:
相對於xml,json,protocol buffer有如下特點:
應用場景:
傳輸數據量大 & 網絡環境不穩定的數據存儲 、rpc數據交換的需求場景。
序列化原理解析:
序列化本質:對數據進行編碼 + 存儲
1. 序列化速度快:
編碼/解碼 方式簡單(只需簡單的數學運算= 位移等)
採用pb自身的框架代碼 和編譯器共同完成
2. 序列化後體積小(數據壓縮效果好)
採用了獨特的編碼方式,如Varint, Zigzag等
採用T - L - V 的數據存儲方式,
-
即
Tag - Length - Value
,標識 - 長度 - 字段值 存儲方式,減少了分隔符的使用,數據存儲得緊湊
protocol buffer語言來組織你的protocol buffer數據,包括.proto文件的語法規則以及如何通過.proto文件來生成數據訪問類代碼。
Defining A Message Type(定義一個消息類型)
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
- 語法說明(syntax)前只能是空行或者註釋
- 每個字段由字段限制、字段類型、字段名和編號四部分組成
Specifying Field Types(指定字段類型)
在上面的例子中,該消息定義了三個字段,兩個int32類型和一個string類型的字段
Assigning Tags(賦予編號)
消息中的每一個字段都有一個獨一無二的數值類型的編號。1到15使用一個字節編碼,16到2047使用2個字節編碼,所以應該將編號1到15留給頻繁使用的字段。
可以指定的最小的編號爲1,最大爲2^{29}-1或536,870,911。但是不能使用19000到19999之間的值,這些值是預留給protocol buffer的。
Specifying Field Rules(指定字段限制)
required
:必須賦值的字段optional
:可有可無的字段repeated
:可重複字段(變長字段)
Adding More Message Types(添加更多消息類型)
一個.proto文件可以定義多個消息類型:
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
Adding Comments(添加註釋)
.proto
文件也使用C/C++風格的註釋語法//
message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3; // Number of results to return per page.
}
Reserved Fields(預留字段)
如果消息的字段被移除或註釋掉,但是使用者可能重複使用字段編碼,就有可能導致例如數據損壞、隱私漏洞等問題。一種避免此類問題的方法就是指明這些刪除的字段是保留的。如果有用戶使用這些字段的編號,protocol buffer編譯器會發出告警。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
What‘s Generated From Your .proto?(編譯.proto
文件)
對於C++,每一個.proto
文件經過編譯之後都會對應的生成一個.h
和一個.cc
文件。
Scalar Value Types(類型對照表)
.proto Type | Notes | C++ Type |
---|---|---|
double | double | double |
float | float | float |
int32 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint32 instead. | int32 |
int64 | Uses variable-length encoding. Inefficient for encoding negative numbers – if your field is likely to have negative values, use sint64 instead. | int64 |
uint32 | Uses variable-length encoding. | uint32 |
uint64 | Uses variable-length encoding. | uint64 |
sint32 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int32s. | int32 |
sint64 | Uses variable-length encoding. Signed int value. These more efficiently encode negative numbers than regular int64s. | int64 |
fixed32 | Always four bytes. More efficient than uint32 if values are often greater than 2^28 | uint32 |
fixed64 | Always eight bytes. More efficient than uint64 if values are often greater than 2^56 | uint64 |
sfixed32 | Always four bytes. | int32 |
sfixed64 | Always eight bytes. | int64 |
bool | bool | boolean |
string | A string must always contain UTF-8 encoded or 7-bit ASCII text. | string |
bytes | May contain any arbitrary sequence of bytes. | string |
Default Values(缺省值)
如果沒有指定默認值,則會使用系統默認值,對於string
默認值爲空字符串,對於bool
默認值爲false,對於數值類型
默認值爲0,對於enum
默認值爲定義中的第一個元素,對於repeated
默認值爲空。
Enumerations(枚舉)
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
通過設置可選參數allow_alias
爲true,就可以在枚舉結構中使用別名(兩個值元素值相同)
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
由於枚舉值採用varint編碼,所以爲了提高效率,不建議枚舉值取負數。這些枚舉值可以在其他消息定義中重複使用。
Using Other Message Types(使用其他消息類型)
可以使用一個消息的定義作爲另一個消息的字段類型。
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
Importing Definitions(導入定義)
就像C++的頭文件一樣,你還可以導入其他的.proto文件
import "myproject/other_protos.proto";
如果想要移動一個.proto
文件,但是又不想修改項目中import
部分的代碼,可以在文件原先位置留一個空.proto
文件,然後使用import public
導入文件移動後的新位置:
// new.proto
// All definitions are moved here
// old.proto
// This is the proto that all clients are importing.
import public "new.proto";
import "other.proto";
// client.proto
import "old.proto";
// You use definitions from old.proto and new.proto, but not other.proto
Nested Types(嵌套類型)
在protocol中可以定義如下的嵌套類型
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果在另外一個消息中需要使用Result
定義,則可以通過Parent.Type
來使用。
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
protocol支持更深層次的嵌套和分組嵌套,但是爲了結構清晰起見,不建議使用過深層次的嵌套。
message Outer { // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
Updating A Message Type(更新一個數據類型)
在實際的開發中會存在這樣一種應用場景,既消息格式因爲某些需求的變化而不得不進行必要的升級,但是有些使用原有消息格式的應用程序暫時又不能被立刻升級,這便要求我們在升級消息格式時要遵守一定的規則,從而可以保證基於新老消息格式的新老程序同時運行。規則如下:
- 不要修改已經存在字段的標籤號。
- 任何新添加的字段必須是optional和repeated限定符,否則無法保證新老程序在互相傳遞消息時的消息兼容性。
- 在原有的消息中,不能移除已經存在的required字段,optional和repeated類型的字段可以被移除,但是他們之前使用的標籤號必須被保留,不能被新的字段重用。
- int32、uint32、int64、uint64和bool等類型之間是兼容的,sint32和sint64是兼容的,string和bytes是兼容的,fixed32和sfixed32,以及fixed64和sfixed64之間是兼容的,這意味着如果想修改原有字段的類型時,爲了保證兼容性,只能將其修改爲與其原有類型兼容的類型,否則就將打破新老消息格式的兼容性。
- optional和repeated限定符也是相互兼容的。
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 ... }
}
Oneof(其中一個字段類型)
有點類似C++中的聯合,就是消息中的多個字段類型在同一時刻只有一個字段會被使用,使用case()
或WhichOneof()
方法來檢測哪個字段被使用了。
Using Oneof(使用Oneof)
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
你可以添加除repeated
外任意類型的字段到Oneof
定義中
Oneof Features(Oneof特性)
-
oneof字段只有最後被設置的字段纔有效,即後面的set操作會覆蓋前面的set操作
SampleMessage message; message.set_name("name"); CHECK(message.has_name()); message.mutable_sub_message(); // Will clear name field. CHECK(!message.has_name());
- oneof不可以是
repeated
的 - 反射API可以作用於oneof字段
-
如果使用C++要防止內存泄露,即後面的set操作會覆蓋之前的set操作,導致前面設置的字段對象發生析構,要注意字段對象的指針操作
SampleMessage message; SubMessage* sub_message = message.mutable_sub_message(); message.set_name("name"); // Will delete sub_message sub_message->set_... // Crashes her
-
如果使用C++的
Swap()
方法交換兩條oneof消息,兩條消息都不會保存之前的字段SampleMessage msg1; msg1.set_name("name"); SampleMessage msg2; msg2.mutable_sub_message(); msg1.swap(&msg2); CHECK(msg1.has_sub_message()); CHECK(msg2.has_name());
Backwards-compatibility issues(向後兼容)
添加或刪除oneof
字段的時候要注意,如果檢測到oneof
字段的返回值是None
/NOT_SET
,這意味着oneof
沒有被設置或者設置了一個不同版本的oneof
的字段,但是沒有辦法能夠區分這兩種情況,因爲沒有辦法確認一個未知的字段是否是一個oneof
的成員。
Tag Reuse Issues(編號複用問題)
- 刪除或添加字段到oneof:在消息序列化或解析後會丟失一些信息,一些字段將被清空
- 刪除一個字段然後重新添加:在消息序列化或解析後會清除當前設置的oneof字段
- 分割或合併字段:同普通的刪除字段操作
Maps(表映射)
protocol buffers提供了簡介的語法來實現map類型:
map<key_type, value_type> map_field = N;
key_type
可以是除浮點指針或bytes
外的其他基本類型,value_type
可以是任意類型
map<string, Project> projects = 3;
- Map的字段不可以是重複的(repeated)
- 線性順序和map值的的迭代順序是未定義的,所以不能期待map的元素是有序的
- maps可以通過key來排序,數值類型的key通過比較數值進行排序
- 線性解析或者合併的時候,如果出現重複的key值,最後一個key將被使用。從文本格式來解析map,如果出現重複key值則解析失敗。
Backwards compatibility(向後兼容)
map語法下面的表達方式在線性上是等價的,所以即使protocol buffers沒有實現maps數據結構也不會影響數據的處理:
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
包
類似C++的命名空間,用來防止名稱衝突
package foo.bar;
message Open { ... }
你可以使用包說明符來定義你的消息字段:
message Foo {
...
foo.bar.Open open = 1;
...
}
定義服務
如果想在RPC系統中使用消息類型,就需要在.proto
文件中定義RPC服務接口,然後使用編譯器生成對應語言的存根。
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
JSON映射
Proto3支持JSON格式的編碼。編碼後的JSON數據的如果沒有值或值爲空,解析時protocol buffer將會使用默認值,在對JSON編碼時可以節省空間。
proto3 | JSON | JSON example | Notes |
---|---|---|---|
message | object | {"fBar": v, "g": null, …} | Generates JSON objects. Message field names are mapped to lowerCamelCase and become JSON object keys. null is accepted and treated as the default value of the corresponding field type. |
enum | string | "FOO_BAR" | The name of the enum value as specified in proto is used. |
map< K,V> | object | {"k": v, …} | All keys are converted to strings. |
repeated V | array | [v, …] | null is accepted as the empty list []. |
bool | true, false | true, false | |
string | string | "Hello World!" | |
bytes | base64 string | "YWJjMTIzIT8kKiYoKSctPUB+" | |
int32, fixed32, uint32 | number | 1, -10, 0 | JSON value will be a decimal number. Either numbers or strings are accepted. |
int64, fixed64, uint64 | string | "1", "-10" | JSON value will be a decimal string. Either numbers or strings are accepted. |
float, double | number | 1.1, -10.0, 0, "NaN", "Infinity" | JSON value will be a number or one of the special string values "NaN", "Infinity", and "-Infinity". Either numbers or strings are accepted. Exponent notation is also accepted. |
Any | object | {"@type": "url", "f": v, … } | If the Any contains a value that has a special JSON mapping, it will be converted as follows: {"@type": xxx,<wbr style="box-sizing: inherit;"> "value": yyy} . Otherwise, the value will be converted into a JSON object, and the "@type" field will be inserted to indicate the actual data type. |
Timestamp | string | "1972-01-01T10:00:20.021Z" | Uses RFC 3339, where generated output will always be Z-normalized and uses 0, 3, 6 or 9 fractional digits. |
Duration | string | "1.000340012s", "1s" | Generated output always contains 0, 3, 6, or 9 fractional digits, depending on required precision. Accepted are any fractional digits (also none) as long as they fit into nano-seconds precision. |
Struct | object | { … } | Any JSON object. See struct.proto. |
Wrapper types | various types | 2, "2", "foo", true, "true", null, 0, … | Wrappers use the same representation in JSON as the wrapped primitive type, except that null is allowed and preserved during data conversion and transfer. |
FieldMask | string | "f.fooBar,h" | See fieldmask.proto. |
ListValue | array | [foo, bar, …] | |
Value | value | Any JSON value | |
NullValue | null | JSON null |
選項
Protocol Buffer允許我們在.proto文件中定義一些常用的選項,這樣可以指示Protocol Buffer編譯器幫助我們生成更爲匹配的目標語言代碼。Protocol Buffer內置的選項被分爲以下三個級別:
文件級別,這樣的選項將影響當前文件中定義的所有消息和枚舉。
消息級別,這樣的選項僅影響某個消息及其包含的所有字段。
字段級別,這樣的選項僅僅響應與其相關的字段。
下面將給出一些常用的Protocol Buffer選項。
-
optimize_for
(文件選項):可以設置的值有SPEED
、CODE_SIZE
或LITE_RUNTIME
,不同的選項會以下述方式影響C++代碼的生成(option optimize_for = CODE_SIZE;
)。SPEED (default): protocol buffer編譯器將會生成序列化,語法分析和其他高效操作消息類型的方式.這也是最高的優化選項.確定是生成的代碼比較大. CODE_SIZE: protocol buffer編譯器將會生成最小的類,確定是比SPEED運行要慢 LITE_RUNTIME: protocol buffer編譯器將會生成只依賴"lite" runtime library (libprotobuf-lite instead of libprotobuf)的類. lite運行時庫比整個庫更小但是刪除了例如descriptors 和 reflection等特性. 這個選項通常用於手機平臺的優化.
cc_enable_arenas
(文件選項):生成的C++代碼啓用arena allocation內存管理-
deprecated
(文件選項):