Android技術(shù)周報(bào)190317期 —— Protocbuf全面解析

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::kFirstReservedNumberFieldDescriptor::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文件。

【參考文章】

  1. gRPC快速入門(一)——Protobuf簡(jiǎn)介
  2. [翻譯] ProtoBuf 官方文檔(二)- 語(yǔ)法指引(proto2)
  3. Protobuf學(xué)習(xí)
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容