Protobuf即Protocol Buffers,是Google公司開(kāi)發(fā)的一種跨語(yǔ)言和平臺(tái)的序列化數(shù)據(jù)結(jié)構(gòu)的方式,是一個(gè)靈活的、高效的用于序列化數(shù)據(jù)的協(xié)議。與XML和JSON格式相比,protobuf更小、更快、更便捷。而且Protobuf是跨語(yǔ)言的,并且自帶一個(gè)編譯器(protoc),只需要用protoc進(jìn)行編譯,就可以編譯成Java、Python、C++、C#、Go等多種語(yǔ)言代碼,然后可以直接使用,不需要再寫其它代碼,自帶有解析的代碼。
一. Protocbuf編譯器的安裝
從https://github.com/protocolbuffers/protobuf/releases上下載最新的release包,并解壓到你的安裝目錄下,然后將其下面的bin目錄添加為系統(tǒng)環(huán)境變量,如mac操作系統(tǒng)下:
export PATH=$PATH:/usr/local/protobuf/bin
這樣就可以在任意地方的命令行窗口下使用protoc指令,如檢查protoc編譯器版本:
protoc --version
二. Protocbuf3語(yǔ)法
1. 版本聲明
syntax = "proto3";
.proto文件中非注釋非空的第一行必須使用Proto版本聲明,如果不使用proto3版本聲明,Protobuf編譯器默認(rèn)使用proto2版本。
2. Package
package com.aervon.proto;
.proto文件中可以新增一個(gè)可選的package聲明符,用來(lái)防止不同的消息類型有命名沖突。包的聲明符會(huì)根據(jù)使用語(yǔ)言的不同影響生成的代碼:
A、對(duì)于C++語(yǔ)言,產(chǎn)生的類會(huì)被包裝在C++的命名空間中。
B、對(duì)于Java語(yǔ)言,包聲明符會(huì)變?yōu)閖ava的一個(gè)包,除非在.proto文件中提供了一個(gè)明確有java_package。
C、對(duì)于Go語(yǔ)言,包可以被用做Go包名稱,除非顯式的提供一個(gè)option go_package在.proto文件中。
3. Import
import "google/protobuf/timestamp.proto";
通過(guò)import聲明符可以引用其他.proto里的結(jié)構(gòu)數(shù)據(jù)體,如以上聲明后就可以使用google.protobuf.Timestamp了。
4. 消息定義
message Person {
string name = 1;
int32 id = 2 [default = 0];
string email = 3;
}
Protobuf中,消息即結(jié)構(gòu)化數(shù)據(jù)。其中變量的聲明結(jié)構(gòu)為:
字段規(guī)則 + 字段類型 + 字段名稱 + [=] + 標(biāo)識(shí)符 + [默認(rèn)值]
字段規(guī)則有:
required: 結(jié)構(gòu)體必須包含該字段一次
optional: 結(jié)構(gòu)體可以包含該字段零次或一次(不超過(guò)一次)
repeated: 該字段可以在格式良好的消息中重復(fù)任意多次(包括0),其中重復(fù)值的順序會(huì)被保留,相當(dāng)于數(shù)組
PS: 在 proto3 中已經(jīng)為兼容性徹底拋棄 required
字段類型可以具有以下幾種類型,在編譯器作用下會(huì)自動(dòng)生成類中的相應(yīng)類型:
| .proto Type | Notes | C++ Type | Java Type | Python Type | Go Type |
|---|---|---|---|---|---|
| double | double | double | float | *float64 | |
| float | float | float | float | *float32 | |
| int32 | 使用可變長(zhǎng)度編碼。編碼負(fù)數(shù)的效率低 - 如果你的字段可能有負(fù)值,請(qǐng)改用 sint32 | int32 | int | int | *int32 |
| int64 | 使用可變長(zhǎng)度編碼。編碼負(fù)數(shù)的效率低 - 如果你的字段可能有負(fù)值,請(qǐng)改用 sint64 | int64 | long | int/long | *int64 |
| uint32 | 使用可變長(zhǎng)度編碼 | uint32 | int | int/long | *uint32 |
| uint64 | 使用可變長(zhǎng)度編碼 | uint64 | long | int/long | *uint64 |
| sint32 | 使用可變長(zhǎng)度編碼。有符號(hào)的 int 值。這些比常規(guī) int32 對(duì)負(fù)數(shù)能更有效地編碼 | int32 | int | int | *int32 |
| sint64 | 使用可變長(zhǎng)度編碼。有符號(hào)的 int 值。這些比常規(guī) int64 對(duì)負(fù)數(shù)能更有效地編碼 | int64 | long | int/long | *int64 |
| fixed32 | 總是四個(gè)字節(jié)。如果值通常大于 228,則比 uint32 更有效。 | uint32 | int | int/long | *uint32 |
| fixed64 | 總是八個(gè)字節(jié)。如果值通常大于 256,則比 uint64 更有效。 | uint64 | long | int/long | *uint64 |
| sfixed32 | 總是四個(gè)字節(jié) | int32 | int | int | *int32 |
| sfixed64 | 總是八個(gè)字節(jié) | int64 | long | int/long | *int64 |
| bool | bool | boolean | bool | *bool | |
| string | 字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本 | string | String | str/unicode | *string |
| bytes | 可以包含任意字節(jié)序列 | string | ByteString | str | []byte |
標(biāo)識(shí)符:
在消息定義中,每個(gè)字段都有唯一的一個(gè)數(shù)字標(biāo)識(shí)符。標(biāo)識(shí)符用來(lái)在消息的二進(jìn)制格式中識(shí)別各個(gè)字段,一旦使用就不能夠再改變。最小的標(biāo)識(shí)符可以從1開(kāi)始,最大到2^29 - 1(536,870,911),不可以使用其中[19000-19999]( Protobuf協(xié)議實(shí)現(xiàn)中進(jìn)行了預(yù)留,從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的標(biāo)識(shí)號(hào)。如果非要在.proto文件中使用預(yù)留標(biāo)識(shí)符,編譯時(shí)就會(huì)報(bào)警。
PS: [1,15]內(nèi)的標(biāo)識(shí)號(hào)在編碼的時(shí)候會(huì)占用一個(gè)字節(jié)。[16,2047]之內(nèi)的標(biāo)識(shí)號(hào)則占用2個(gè)字節(jié)。所以應(yīng)該為頻繁出現(xiàn)的消息元素保留[1,15]內(nèi)的標(biāo)識(shí)號(hào)。
默認(rèn)值:
當(dāng)一個(gè)消息被解析的時(shí)候,如果編碼消息里不包含一個(gè)特定的singular元素,被解析的對(duì)象所對(duì)應(yīng)的字段被設(shè)置為一個(gè)默認(rèn)值,不同類型默認(rèn)值如下:
| 變量類型 | 默認(rèn)值 |
|---|---|
| string | 空string |
| bytes | 空bytes |
| bool | false |
| 數(shù)值類型 | 0 |
| 枚舉 | 第一個(gè)定義的枚舉值 |
| 消息類型(message) | 字段沒(méi)有被設(shè)置,確切的消息是根據(jù)語(yǔ)言確定的,通常情況下是對(duì)應(yīng)語(yǔ)言中空列表 |
| 標(biāo)量消息字段 | 一旦消息被解析,就無(wú)法判斷字段是被設(shè)置為默認(rèn)值還是根本沒(méi)有被設(shè)置,應(yīng)該在定義消息類型時(shí)注意 |
5. 添加注釋
為你的 .proto 文件添加注釋,可以使用 C/C++ 語(yǔ)法風(fēng)格的注釋 // 和 /* ... */ 。
/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */
message SearchRequest {
required string query = 1;
optional int32 page_number = 2; // Which page number do we want?
optional int32 result_per_page = 3; // Number of results to return per page.
}
6. Reserved 保留字段
如果你通過(guò)完全刪除字段或?qū)⑵渥⑨尩魜?lái)更新 message 類型,則未來(lái)一些用戶在做他們的修改或更新時(shí)就可能會(huì)再次使用這些字段編號(hào)。如果以后加載相同 .proto 的舊版本,這可能會(huì)導(dǎo)致一些嚴(yán)重問(wèn)題,包括數(shù)據(jù)損壞,隱私錯(cuò)誤等。確保不會(huì)發(fā)生這種情況的一種方法是指定已刪除字段的字段編號(hào)(有時(shí)也需要指定名稱為保留狀態(tài),英文名稱可能會(huì)導(dǎo)致 JSON 序列化問(wèn)題)為 “保留” 狀態(tài)。如果將來(lái)的任何用戶嘗試使用這些字段標(biāo)識(shí)符,protocol buffer 編譯器將會(huì)抱怨。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
PS: 你不能在同一 "reserved" 語(yǔ)句中將字段名稱和字段編號(hào)混合在一起指定。
7. 枚舉 Enumerations
在下面的例子中,我們添加了一個(gè)名為 Corpus 的枚舉,其中包含所有可能的值,之后定義了一個(gè)類型為 Corpus 枚舉的字段:
message SearchRequest {
required string query = 1;
optional int32 page_number = 2;
optional int32 result_per_page = 3 [default = 10];
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
optional Corpus corpus = 4 [default = UNIVERSAL];
}
你可以通過(guò)為不同的枚舉常量指定相同的值來(lái)定義別名。為此,你需要將 allow_alias 選項(xiàng)設(shè)置為true,否則 protocol 編譯器將在找到別名時(shí)生成錯(cuò)誤消息。
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // 取消此行注釋將導(dǎo)致 Google 內(nèi)部的編譯錯(cuò)誤和外部的警告消息
}
枚舉器常量必須在 32 位整數(shù)范圍內(nèi)。由于 enum 值在線上使用 varint encoding ,負(fù)值效率低,因此不推薦使用。你可以在 message 中定義 enums,如上例所示的那樣?;蛘邔⑵涠x在 message 外部 - 這樣這些 enum 就可以在 .proto 文件中的任何 message 定義中重用。你還可以使用一個(gè) message 中聲明的 enum 類型作為不同 message 中字段的類型,使用語(yǔ)法 MessageType.EnumType 來(lái)實(shí)現(xiàn)。
當(dāng)你在使用 enum 的 .proto 上運(yùn)行 protocol buffer 編譯器時(shí),生成的代碼將具有相應(yīng)的用于 Java 或 C++ 的 enum,或者用于創(chuàng)建集合的 Python 的特殊 EnumDescriptor 類。運(yùn)行時(shí)生成的類中具有整數(shù)值的符號(hào)常量。
8. 擴(kuò)展 Extensions
通過(guò)擴(kuò)展,你可以聲明 message 中的一系列字段編號(hào)用于第三方擴(kuò)展。擴(kuò)展名是那些未由原始 .proto 文件定義的字段的占位符。這允許通過(guò)使用這些字段編號(hào)來(lái)定義部分或全部字段從而將其它 .proto 文件定義的字段添加到當(dāng)前 message 定義中。我們來(lái)看一個(gè)例子:
message Foo {
// ...
extensions 100 to 199;
}
這表示 Foo 中的字段數(shù) [100,199] 的范圍是為擴(kuò)展保留的。其他用戶現(xiàn)在可以使用指定范圍內(nèi)的字段編號(hào)在他們自己的 .proto 文件中為 Foo 添加新字段,例如:
extend Foo {
optional int32 bar = 126;
}
這會(huì)將名為 bar 且編號(hào)為 126 的字段添加到 Foo 的原始定義中。
當(dāng)用戶的 Foo 消息被編碼時(shí),其格式與用戶在 Foo 中常規(guī)定義新字段的格式完全相同。但是,在應(yīng)用程序代碼中訪問(wèn)擴(kuò)展字段的方式與訪問(wèn)常規(guī)字段略有不同 - 生成的數(shù)據(jù)訪問(wèn)代碼具有用于處理擴(kuò)展的特殊訪問(wèn)器。那么,舉個(gè)例子,下面就是如何在 C++ 中設(shè)置 bar 的值:
Foo foo;
foo.SetExtension(bar, 15);
類似地,F(xiàn)oo 類定義模板化訪問(wèn)器 HasExtension(),ClearExtension(),GetExtension(),MutableExtension() 和 AddExtension()。它們都具有與正常字段生成的訪問(wèn)器相匹配的語(yǔ)義。有關(guān)使用擴(kuò)展的更多信息,請(qǐng)參閱所選語(yǔ)言的代碼生成參考。
PS:擴(kuò)展可以是任何字段類型,包括 message 類型,但不能是 oneofs 或 maps。
另外,你也可以在另一種 message 類型內(nèi)部聲明擴(kuò)展:
message Baz {
extend Foo {
optional int32 bar = 126;
}
}
在這種情況下,訪問(wèn)此擴(kuò)展的 C++ 代碼為:
Foo foo;
foo.SetExtension(Baz::bar, 15);
換句話說(shuō),唯一的影響是 bar 是在 Baz 的范圍內(nèi)定義。
如果你的編號(hào)約定可能涉及那些具有非常大字段編號(hào)的擴(kuò)展,則可以使用 max 關(guān)鍵字指定擴(kuò)展范圍至編號(hào)最大值:
message Foo {
extensions 1000 to max;
}
最大值為 229 - 1,或者 536,870,911。與一般選擇字段編號(hào)時(shí)一樣,你的編號(hào)約定還需要避免 19000 到 19999 的字段編號(hào)(FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber),因?yàn)樗鼈兪菫?Protocol Buffers 實(shí)現(xiàn)保留的。你可以定義包含此范圍的擴(kuò)展名范圍,但 protocol 編譯器不允許你使用這些編號(hào)定義實(shí)際擴(kuò)展名。
9. Any類型
Any類型消息允許在沒(méi)有指定.proto定義的情況下使用消息作為一個(gè)嵌套類型。一個(gè)Any類型包括一個(gè)可以被序列化bytes類型的任意消息以及一個(gè)URL作為一個(gè)全局標(biāo)識(shí)符和解析消息類型。
為了使用Any類型,需要導(dǎo)入import google/protobuf/any.proto。
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
對(duì)于給定的消息類型的默認(rèn)類型URL是type.googleapis.com/packagename.messagename。
不同語(yǔ)言的實(shí)現(xiàn)會(huì)支持動(dòng)態(tài)庫(kù)以線程安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any類型會(huì)有特殊的pack()和unpack()訪問(wèn)器,在C++中會(huì)有PackFrom()和UnpackTo()方法。
10. Oneof
Oneof定義用來(lái)代表在實(shí)現(xiàn)的時(shí)候,該組屬性中有且只能有一個(gè)被定義,不能出現(xiàn)多個(gè)。
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
上述定義中只能出現(xiàn)name或者sub_message的出現(xiàn),不能同時(shí)出現(xiàn),同時(shí)Oneof不能出現(xiàn)repeated域。重復(fù)傳遞值到Oneof多個(gè)域僅僅最后的會(huì)生效,其它的將被忽略掉。
11. Map
如果要?jiǎng)?chuàng)建一個(gè)關(guān)聯(lián)映射,Protobuf提供了一種快捷的語(yǔ)法:
map<key_type, value_type> map_field = N;
其中key_type可以是任意Integer或者string類型(除了floating和bytes的任意標(biāo)量類型都可以),value_type可以是任意類型,但不能是map類型。例如,創(chuàng)建一個(gè)Project的映射,每個(gè)Projecct使用一個(gè)string作為key:
map<string, Project> projects = 3;
Map的字段可以是repeated。序列化后的順序和map迭代器的順序是不確定的,所以不要期望以固定順序處理Map。當(dāng)為.proto文件產(chǎn)生生成文本格式的時(shí)候,map會(huì)按照key 的順序排序,數(shù)值化的key會(huì)按照數(shù)值排序。
從序列化中解析或者融合時(shí),如果有重復(fù)的key則后一個(gè)key不會(huì)被使用。
PS: map語(yǔ)法序列化后等同于如下內(nèi)容,因此即使是不支持map語(yǔ)法的Protobuf實(shí)現(xiàn)也可以處理數(shù)據(jù):
message MapFieldEntry {
key_type key = 1;
value_type value = 2;
}
repeated MapFieldEntry map_field = N;
12. 定義服務(wù)
如果想要將消息類型用在RPC(遠(yuǎn)程方法調(diào)用)系統(tǒng)中,可以在.proto文件中定義一個(gè)RPC服務(wù)接口,Protobuf編譯器將會(huì)根據(jù)所選擇的不同語(yǔ)言生成服務(wù)接口代碼及stub。如要定義一個(gè)RPC服務(wù)并具有一個(gè)方法Search,Search方法能夠接收SearchRequest并返回一個(gè)SearchResponse,可以在.proto文件中進(jìn)行如下定義:
service SearchService {
rpc Search (SearchRequest) returns (SearchResponse);
}
最直觀的使用Protobuf的RPC系統(tǒng)是gRPC,由谷歌開(kāi)發(fā)的語(yǔ)言和平臺(tái)中的開(kāi)源的PRC系統(tǒng),gRPC在使用Protobuf時(shí)非常有效,如果使用特殊的Protobuf插件可以直接從.proto文件中產(chǎn)生相關(guān)的RPC代碼。
如果不想使用gRPC,可以使用Protobuf用于自己的RPC實(shí)現(xiàn)。
三. 編譯Protoc文件
Protobuf提供了protoc編譯器,用于通過(guò)定義好的.proto文件來(lái)生成Java,Python,C++,Ruby,Objective-C,C#,Go等語(yǔ)言代碼。
protoc --proto_path=IMPORT_PATH --cpp_out=DST_DIR --java_out=DST_DIR --python_out=DST_DIR --go_out=DST_DIR --ruby_out=DST_DIR --javanano_out=DST_DIR --objc_out=DST_DIR --csharp_out=DST_DIR path/to/file.proto
1. 導(dǎo)入目錄設(shè)置
IMPORT_PATH聲明了一個(gè).proto文件所在的解析import具體目錄。如果忽略該值,則使用當(dāng)前目錄。如果有多個(gè)目錄則可以多次調(diào)用--proto_path,會(huì)順序的被訪問(wèn)并執(zhí)行導(dǎo)入。-I=IMPORT_PATH是--proto_path的簡(jiǎn)化形式。
2. 生成代碼指定
--cpp_out :在目標(biāo)目錄DST_DIR中產(chǎn)生C++代碼
--java_out :在目標(biāo)目錄DST_DIR中產(chǎn)生Java代碼
--python_out :在目標(biāo)目錄 DST_DIR 中產(chǎn)生Python代碼
--go_out :在目標(biāo)目錄 DST_DIR 中產(chǎn)生Go代碼
--ruby_out:在目標(biāo)目錄 DST_DIR 中產(chǎn)生Ruby代碼
--javanano_out:在目標(biāo)目錄DST_DIR中生成JavaNano
--objc_out:在目標(biāo)目錄DST_DIR中產(chǎn)生Object代碼
--csharp_out:在目標(biāo)目錄DST_DIR中產(chǎn)生Object代碼
--php_out:在目標(biāo)目錄DST_DIR中產(chǎn)生Object代碼
3. 導(dǎo)入proto消息文件指定
必須指定一個(gè)或多個(gè).proto文件作為輸入,多個(gè).proto文件可以只指定一次。雖然文件路徑是相對(duì)于當(dāng)前目錄的,每個(gè)文件必須位于其IMPORT_PATH下,以便每個(gè)文件可以確定其規(guī)范的名稱。
4. 生成編程語(yǔ)言相關(guān)代碼
當(dāng)用Protobuf編譯器來(lái)運(yùn)行.proto文件時(shí),編譯器將生成所選擇語(yǔ)言的代碼,相應(yīng)語(yǔ)言的代碼可以操作在.proto文件中定義的消息類型,包括獲取、設(shè)置字段值,將消息序列化到一個(gè)輸出流中以及從一個(gè)輸入流中解析消息。
對(duì)C++語(yǔ)言,編譯器會(huì)為每個(gè).proto文件生成一個(gè).h文件和一個(gè).cc文件,.proto文件中的每一個(gè)消息有一個(gè)對(duì)應(yīng)的類。
對(duì)Java語(yǔ)言,編譯器為每一個(gè)消息類型生成了一個(gè).java文件以及一個(gè)特殊的Builder類(用來(lái)創(chuàng)建消息類接口的)。
對(duì)Go語(yǔ)言,編譯器會(huì)為每個(gè)消息類型生成了一個(gè).pb.go文件。
對(duì)Ruby語(yǔ)言,編譯器會(huì)為每個(gè)消息類型生成了一個(gè).rb文件。
【參考文章】