1. 程式人生 > >gRPC快速入門(一)——Protobuf簡介

gRPC快速入門(一)——Protobuf簡介

gRPC快速入門(一)——Protobuf簡介

一、Protobuf簡介

1、Protobuf簡介

Protobuf即Protocol Buffers,是Google公司開發的一種跨語言和平臺的序列化資料結構的方式,是一個靈活的、高效的用於序列化資料的協議。
與XML和JSON格式相比,protobuf更小、更快、更便捷。protobuf是跨語言的,並且自帶一個編譯器(protoc),只需要用protoc進行編譯,就可以編譯成Java、Python、C++、C#、Go等多種語言程式碼,然後可以直接使用,不需要再寫其它程式碼,自帶有解析的程式碼。
只需要將要被序列化的結構化資料定義一次(在.proto檔案定義),便可以使用特別生成的原始碼(使用protobuf提供的生成工具)輕鬆的使用不同的資料流完成對結構資料的讀寫操作。甚至可以更新.proto檔案中對資料結構的定義而不會破壞依賴舊格式編譯出來的程式。
GitHub地址:

https://github.com/protocolbuffers/protobuf
不同語言原始碼版本下載地址:
https://github.com/protocolbuffers/protobuf/releases/latest

2、Protobuf的優缺點

Protobuf的優點如下:
A、效能號,效率高
序列化後位元組佔用空間比XML少3-10倍,序列化的時間效率比XML快20-100倍。
B、有程式碼生成機制
將對結構化資料的操作封裝成一個類,便於使用。
C、支援向後和向前相容
當客戶端和伺服器同時使用一塊協議的時候, 當客戶端在協議中增加一個位元組,並不會影響客戶端的使用
D、支援多種程式語言
Protobuf目前已經支援Java,C++,Python、Go、Ruby等多種語言。

Protobuf的缺點如下:
A、二進位制格式導致可讀性差
B、缺乏自描述

二、Protobuf編譯器安裝

1、C++版本Protobuf編譯器安裝

下載C++版本的Protobuf原始碼protobuf-cpp-3.6.1.tar.gz
解壓Protobuf原始碼:
tar -zxvf protobuf-cpp-3.6.1.tar.gz
進入protobuf-3.6.1原始碼目錄:
cd protobuf-3.6.1
配置變數:
./configure --prefix=/usr/local/protobuf
編譯:
make
檢查、測試:
make check
安裝:
sudo make install
設定環境變數:

export LD_LIBRARY_PATH=$LD_LIBRARY_PATH:/usr/local/protobuf/lib
export LIBRARY_PATH=$LIBRARY_PATH:/usr/local/protobuf/lib
export PATH=$PATH:/usr/local/protobuf/bin

檢查版本號:
protoc --version

2、Protobuf編譯器使用

Protobuf提供了protoc編譯器,用於通過定義好的.proto檔案來生成Java,Python,C++,Ruby,Objective-C,C#,Go等語言程式碼。
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)匯入目錄設定
IMPORT_PATH聲明瞭一個.proto檔案所在的解析import具體目錄。如果忽略該值,則使用當前目錄。如果有多個目錄則可以多次呼叫--proto_path,會順序的被訪問並執行匯入。-I=IMPORT_PATH是--proto_path的簡化形式。
(2)生成程式碼指定

--cpp_out :在目標目錄DST_DIR中產生C++程式碼
--java_out :在目標目錄DST_DIR中產生Java程式碼
--python_out :在目標目錄 DST_DIR 中產生Python程式碼
--go_out :在目標目錄 DST_DIR 中產生Go程式碼
--ruby_out:在目標目錄 DST_DIR 中產生Ruby程式碼
--javanano_out:在目標目錄DST_DIR中生成JavaNano
--objc_out:在目標目錄DST_DIR中產生Object程式碼
--csharp_out:在目標目錄DST_DIR中產生Object程式碼
 --php_out:在目標目錄DST_DIR中產生Object程式碼

(3)匯入proto訊息檔案指定
必須指定一個或多個.proto檔案作為輸入,多個.proto檔案可以只指定一次。雖然檔案路徑是相對於當前目錄的,每個檔案必須位於其IMPORT_PATH下,以便每個檔案可以確定其規範的名稱。
(4)生成程式語言相關程式碼
當用Protobuf編譯器來執行.proto檔案時,編譯器將生成所選擇語言的程式碼,相應語言的程式碼可以操作在.proto檔案中定義的訊息型別,包括獲取、設定欄位值,將訊息序列化到一個輸出流中以及從一個輸入流中解析訊息。
對C++語言,編譯器會為每個.proto檔案生成一個.h檔案和一個.cc檔案,.proto檔案中的每一個訊息有一個對應的類。
對Java語言,編譯器為每一個訊息型別生成了一個.java檔案以及一個特殊的Builder類(用來建立訊息類介面的)。
對Go語言,編譯器會為每個訊息型別生成了一個.pb.go檔案。
對Ruby語言,編譯器會為每個訊息型別生成了一個.rb檔案。

三、Protobuf3語法

1、訊息定義

Protobuf中,訊息即結構化資料。

message Person {
  string name = 1;
  int32 id = 2;  
  string email = 3;
}

Person訊息格式有3個欄位,在訊息中承載的資料分別對應於每一個欄位,其中每個欄位都有一個名字和一種型別。
在一個訊息檔案.proto中可以定義多個訊息型別,在定義多個相關的訊息的時候較為有用。

// [START declaration]
syntax = "proto3";
package Company.Person;

import "google/protobuf/timestamp.proto";
// [END declaration]

// [START messages]
message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

// Our address book file is just one of these.
message AddressBook {
  repeated Person people = 1;
}
// [END messages]

.proto檔案中非註釋非空的第一行必須使用Proto版本宣告,版本宣告如下:
syntax = "proto3";
如果不使用proto3版本宣告,Protobuf編譯器預設使用proto2版本。
Proto訊息檔案的命名如下:
packageName.MessageName.proto
packageName為package宣告的包名
MessageName為訊息名稱

2、添加註釋

添加註釋可以使用C/C++/java風格的雙斜槓(//)語法格式。

3、Package

.proto檔案中可以新增一個可選的package宣告符,用來防止不同的訊息型別有命名衝突。包的宣告符會根據使用語言的不同影響生成的程式碼:
A、對於C++語言,產生的類會被包裝在C++的名稱空間中。
B、對於Java語言,包宣告符會變為java的一個包,除非在.proto檔案中提供了一個明確有java_package。
C、對於Go語言,包可以被用做Go包名稱,除非顯式的提供一個option go_package在.proto檔案中。
Protobuf語法中型別名稱的解析與C++是一致的:首先從最內部開始查詢,依次向外進行,每個包會被看作是其父類包的內部類。當然對於Company.Person以“.”分隔的是從最外圍開始的。
Protobuf編譯器會解析.proto檔案中定義的所有型別名。 對於不同語言的程式碼生成器會知道如何來指向每個具體的型別,即使它們使用了不同的規則。

4、欄位型別

欄位型別包括標量型別和合成型別。
標量型別包括:
gRPC快速入門(一)——Protobuf簡介
合成型別包括列舉(enumerations)或其它訊息型別。

5、識別符號

在訊息定義中,每個欄位都有唯一的一個數字識別符號。識別符號用來在訊息的二進位制格式中識別各個欄位,一旦使用就不能夠再改變。
最小的識別符號可以從1開始,最大到2^29 - 1(536,870,911),不可以使用其中[19000-19999]( Protobuf協議實現中進行了預留,從FieldDescriptor::kFirstReservedNumber 到 FieldDescriptor::kLastReservedNumber)的標識號。如果非要在.proto檔案中使用預留識別符號,編譯時就會報警。
[1,15]內的標識號在編碼的時候會佔用一個位元組。[16,2047]之內的標識號則佔用2個位元組。所以應該為頻繁出現的訊息元素保留[1,15]內的標識號。

6、欄位規則

訊息的欄位修飾符必須是如下之一:
A、singular:一個格式良好的message應該有0個或者1個該欄位(但不能超過1個)。
B、repeated:在一個格式良好的訊息中,該欄位可以重複任意多次(包括0次),重複值的順序會被保留。
在proto3中,repeated的標量欄位預設情況下使用packed。

7、保留識別符號

如果通過刪除或者註釋所有欄位,以後的使用者在更新訊息型別的時候可能重用識別符號。如果使用舊版本程式碼載入相同的.proto檔案會導致嚴重的問題,包括資料損壞、隱私錯誤等等。為了確保不會發生向前相容可以為欄位tag(reserved name可能會JSON序列化的問題)指定reserved識別符號,Protobuf編譯器會警告未來嘗試使用相應欄位識別符號的使用者。
不要在同一行reserved宣告中同時宣告欄位名字和識別符號。

message Foo {
    reserved 2, 15, 9 to 11;
    reserved "foo", "bar";
}

8、預設值

當一個訊息被解析的時候,如果編碼訊息裡不包含一個特定的singular元素,被解析的物件所對應的欄位被設定為一個預設值,不同型別預設值如下:
對於string,預設是一個空string
對於bytes,預設是一個空的bytes
對於bool,預設是false
對於數值型別,預設是0
對於列舉,預設是第一個定義的列舉值,必須為0
對於訊息型別(message),欄位沒有被設定,確切的訊息是根據語言確定的,通常情況下是對應語言中空列表。
對於標量訊息欄位,一旦訊息被解析,就無法判斷欄位是被設定為預設值還是根本沒有被設定,應該在定義訊息型別時注意。

9、列舉

當定義一個訊息型別時,需要為訊息中的某個欄位指定預定義值序列中的一個值,此時可以使用列舉定義預定以序列。如為Person訊息新增一個PhoneType型別的欄位,PhoneType型別的值可能是MOBILE,HOME,WORK。

 message Person {
  string name = 1;
  int32 id = 2;  // Unique ID number for this person.
  string email = 3;
  enum PhoneType {
    MOBILE = 0;
    HOME = 1;
    WORK = 2;
  }
  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }

  repeated PhoneNumber phones = 4;

  google.protobuf.Timestamp last_updated = 5;
}

每個列舉型別必須將其第一個型別對映為0。
可以通過allow_alias選項為true,將不同的列舉常量指定為相同的值,否則編譯器會在別名的地方產生一個錯誤資訊。

enum EnumAllowingAlias {
    option allow_alias = true;
    UNKNOWN = 0;
    STARTED = 1;
    RUNNING = 1;
}
enum EnumNotAllowingAlias {
    UNKNOWN = 0;   //EnumNotAllowingAlias中沒有設定allow_alias
    STARTED = 1;
    // RUNNING = 1;//error 
}

列舉常量必須在32位整型值的範圍內。因為enum值是使用可變編碼方式的,對負數不夠高效,因此不推薦在enum中使用負數。
可以在一個訊息定義的內部或外部定義列舉,列舉可以在.proto檔案中的任何訊息定義裡重用。可以在一個訊息中宣告一個列舉型別,而在另一個不同的訊息中使用列舉(採用MessageType.EnumType的語法格式)。
當對一個使用了列舉的.proto檔案執行Protobuf編譯器的時候,生成的程式碼中將有一個對應的enum(Java或C++),被用來在執行時生成的類中建立一系列的整型值符號常量(symbolic constants)。
在反序列化的過程中,無法識別的列舉值會被儲存在訊息中。對支援開放列舉型別超出指定範圍外的語言(例如C++和Go),未識別的值會被表示成所支援的整型;對封閉列舉型別的語言中(Java),使用列舉中的一個型別來表示未識別的值,並且可以使用所支援整型來訪問;在其它情況下,如果解析的訊息被序列號,未識別的值將保持原樣。

10、引用其它訊息型別

可以將其它訊息型別用作欄位型別。對於同一個訊息檔案內部定義的訊息,可以在其它訊息內部直接引用訊息型別;對於在其它訊息檔案定義的訊息型別,可以通過匯入其他訊息檔案中的定義來使用相應的訊息型別。如使用google.protobuf.Timestamp訊息型別需要匯入相應訊息檔案:
import "google/protobuf/timestamp.proto";
如果要在父訊息型別的外部重用訊息型別,需要以Parent.Type的形式使用。

11、Any型別

Any型別訊息允許在沒有指定.proto定義的情況下使用訊息作為一個巢狀型別。一個Any型別包括一個可以被序列化bytes型別的任意訊息以及一個URL作為一個全域性識別符號和解析訊息型別。
為了使用Any型別,需要匯入import google/protobuf/any.proto。

import "google/protobuf/any.proto";
message ErrorStatus {
    string message = 1;
    repeated google.protobuf.Any details = 2;
}

對於給定的訊息型別的預設型別URL是type.googleapis.com/packagename.messagename
不同語言的實現會支援動態庫以執行緒安全的方式去幫助封裝或者解封裝Any值。例如在java中,Any型別會有特殊的pack()和unpack()訪問器,在C++中會有PackFrom()和UnpackTo()方法。

12、Oneof

Oneof定義用來代表在實現的時候,該組屬性中有且只能有一個被定義,不能出現多個。

message SampleMessage {
  oneof test_oneof {
    string name = 4;
    SubMessage sub_message = 9;
  }
}

上述定義中只能出現name或者sub_message的出現,不能同時出現,同時Oneof不能出現repeated域。重複傳遞值到Oneof多個域僅僅最後的會生效,其它的將被忽略掉。

13、Map

如果要建立一個關聯對映,Protobuf提供了一種快捷的語法:

map<key_type, value_type> map_field = N;

其中key_type可以是任意Integer或者string型別(除了floating和bytes的任意標量型別都可以),value_type可以是任意型別,但不能是map型別。
例如,建立一個Project的對映,每個Projecct使用一個string作為key:

map<string, Project> projects = 3;

Map的欄位可以是repeated。
序列化後的順序和map迭代器的順序是不確定的,所以不要期望以固定順序處理Map
當為.proto檔案產生生成文字格式的時候,map會按照key 的順序排序,數值化的key會按照數值排序。
從序列化中解析或者融合時,如果有重複的key則後一個key不會被使用,當從文字格式中解析map時,如果存在重複的key。
向後相容性問題
map語法序列化後等同於如下內容,因此即使是不支援map語法的Protobuf實現也可以處理資料:

message MapFieldEntry {
    key_type key = 1;
    value_type value = 2;
}
repeated MapFieldEntry map_field = N;

14、定義服務

如果想要將訊息型別用在RPC(遠端方法呼叫)系統中,可以在.proto檔案中定義一個RPC服務介面,Protobuf編譯器將會根據所選擇的不同語言生成服務介面程式碼及stub。如要定義一個RPC服務並具有一個方法Search,Search方法能夠接收SearchRequest並返回一個SearchResponse,可以在.proto檔案中進行如下定義:

service SearchService {
    rpc Search (SearchRequest) returns (SearchResponse);
}

最直觀的使用Protobuf的RPC系統是gRPC,由谷歌開發的語言和平臺中的開源的PRC系統,gRPC在使用Protobuf時非常有效,如果使用特殊的Protobuf外掛可以直接從.proto檔案中產生相關的RPC程式碼。
如果不想使用gRPC,可以使用Protobuf用於自己的RPC實現。

15、JSON對映

Proto3支援JSON的編碼規範,便於在不同系統之間共享資料。
如果JSON編碼的資料丟失或者其本身是null,資料會在解析成Protobuf的時候被表示成預設值。如果一個欄位在Protobuf中表示為預設值,會在轉化成JSON編碼的時候忽略掉以節省空間。
gRPC快速入門(一)——Protobuf簡介

16、更新訊息型別

如果一個已有的訊息格式已無法滿足新的需求,需要在要息中新增一個額外的欄位,但同時舊版本寫的程式碼仍然可用。可以使用更新訊息解決,更新訊息而不破壞已有程式碼是非常簡單的。更新訊息時規則如下:
A、不要更改任何已有欄位的識別符號。
B、如果增加新的欄位,使用舊格式的欄位仍然可以被新產生的程式碼所解析。應該記住元素的預設值,新程式碼就可以以適當的方式和舊程式碼產生的資料互動。通過新程式碼產生的訊息也可以被舊程式碼解析,但新增加的欄位會被忽視掉。未被識別的欄位會在反序列化的過程中丟棄掉,如果訊息再被傳遞給新的程式碼,新的欄位依然是不可用的。
C、非required的欄位可以移除。只要識別符號在新的訊息型別中不再使用(推薦重新命名欄位,例如在欄位前新增“OBSOLETE_”字首)。
D、int32, uint32, int64, uint64,和bool是全部相容的,可以相互轉換,而不會破壞向前、 向後的相容性。
E、sint32和sint64是互相相容的,但與其它整數型別不相容。
F、string和bytes是相容的——只要bytes是有效的UTF-8編碼。
G、巢狀訊息與bytes是相容的——只要bytes包含該訊息的一個編碼過的版本。
H、fixed32與sfixed32是相容的,fixed64與sfixed64是相容的。
I、列舉型別與int32,uint32,int64和uint64相相容(注意如果值不相相容則會被截斷),然而在客戶端反序列化後可能會有不同的處理方式,例如,未識別的proto3列舉型別會被保留在訊息中,但表示方式會依照語言而定。int型別的欄位總會保留他們的
J、可以新增新的optional或repeated的欄位, 但必須使用新的識別符號(訊息中從未使用過的識別符號,不能使用已經被刪除過的識別符號)。

17、選項

在定義.proto檔案時能夠標註一系列的options。options並不改變整個檔案宣告的含義,但卻能夠影響特定環境下處理方式。完整的可用選項可以在google/protobuf/descriptor.proto找到。
一些選項是檔案級別的,意味著它可以作用於最外範圍,不包含在任何訊息內部、enum或服務定義中。一些選項是訊息級別的,意味著它可以用在訊息定義的內部。當然有些選項可以作用在域、enum型別、enum值、服務型別及服務方法中。到目前為止,並沒有一種有效的選項能作用於所有的型別
optimize_for(檔案選項): 可以被設定為LITE_RUNTIME,SPEED,CODE_SIZE。這些值將通過如下的方式影響C++及Java程式碼的生成: 
SPEED (default): Protobuf編譯器將通過在訊息型別上執行序列化、語法分析及其它通用的操作,生成的程式碼最優。
CODE_SIZE:Protobuf編譯器將會產生最少量的類,通過共享或基於反射的程式碼來實現序列化、語法分析及各種其它操作。採用CODE_SIZE方式產生的程式碼將比SPEED要少得多,但操作要相對慢些。CODE_SIZE方式生成程式碼中實現的類及其對外的API與SPEED模式都是一樣的,常用在一些包含大量的.proto檔案而且並不盲目追求速度的應用中。
LITE_RUNTIME:Protobuf編譯器依賴於執行時核心類庫來生成程式碼(即採用libprotobuf-lite替代libprotobuf)。libprotobuf-lite核心類庫由於忽略了一些描述符及反射,要比全類庫小得多。這種模式經常在移動手機平臺應用多一些。編譯器採用LITE_RUNTIME模式產生的方法實現與SPEED模式不相上下,產生的類通過實現MessageLite介面,但僅僅是Messager介面的一個子集。
option optimize_for = CODE_SIZE;
cc_enable_arenas(檔案選項):對於C++產生的程式碼啟用arena allocation。
objc_class_prefix(檔案選項):設定Objective-C類的字首,新增到所有Objective-C從此.proto檔案產生的類和列舉型別。沒有預設值,所使用的字首應該是×××薦的3-5個大寫字元,注意2個位元組的字首是蘋果所保留的。
deprecated(欄位選項):如果設定為true則表示該欄位已經被廢棄,並且不應該在新的程式碼中使用。在大多數語言中沒有實際的意義。
int32 old_field = 6 [deprecated=true];
java_package (file option):指定生成java類所在的包,如果在.proto檔案中沒有明確的宣告java_package,會使用預設包名。不需要生成java程式碼時不起作用。
java_outer_classname (file option):指定生成Java類的名稱,如果在.proto檔案中沒有明確宣告java_outer_classname,生成的class名稱將會根據.proto檔案的名稱採用駝峰式的命名方式進行生成。如(foo_bar.proto生成的java類名為FooBar.java),不需要生成java程式碼時不起任何作用
objc_class_prefix (file option): 指定Objective-C類字首,會前置在所有類和列舉型別名之前。沒有預設值,應該使用3-5個大寫字母。注意所有2個字母的字首是Apple保留的。

四、proto檔案編碼規範

Proto檔案編碼規範如下:
A、描述檔案以.proto做為檔案字尾。
B、除結構定義外的語句以分號結尾,結構定義包括:message、service、enum;rpc方法定義結尾的分號可有可無。
C、Message命名採用駝峰命名方式,欄位命名採用小寫字母加下劃線分隔方式。
D、Enums型別名採用駝峰命名方式,欄位命名採用大寫字母加下劃線分隔方式。
E、Service與rpc方法名統一採用駝峰式命名。