Proto3入門
本文基於Google提供的ProtolBuffer LanguageGuide英文文件: ProtolBuffer3 Language Guide
定義一個message
首先以一個簡單的例子開頭:比如查百度,那麼需要一個查詢語句:query,還有查詢的頁面號:page_number,然後就是查詢的每一頁的結果數:result_per_page。 這樣就有三個欄位:query,page_number和result_per_page。 那麼這個訊息(message)定義如下:
/*選中語法格式proto3,也就是ProtocolBuffer的版本3*/
syntax = "proto3";
/*定義一個訊息,訊息名字為SearchRequest*/
message SearchRequest{
/*鍵值對,每個欄位則需要欄位名和具體的型別*/
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
複製程式碼
對於每一個欄位,必須指定具體的型別
除了一個基礎型別(字串string,整型int32等)還可以定義其他綜合型別,如列舉型別和其他的message型別。後面將列出。
分配欄位號
在上面定義的SearchRequest訊息中,對於每個欄位,都有唯一標識的編號。這些欄位號在訊息(message)的二進位制格式中唯一識別,在該訊息(message)投入到使用後不應該被更改。
在ProtocolBuffer中將訊息序列化為二進位制後,對於1~15
編號的欄位,只需要一個位元組編碼,對於16~2047
則需要兩個位元組。所以,對於把經常使用的欄位元素編號到1~15
中,並且預留(reserved)幾位以便於以後擴充套件。
欄位號的範圍為:1~536870911(2^29-1)
。其中19000~19999
為ProtocolBuffer自己預留(reserved)的欄位號不能使用。其他都可以自己使用。當然,自己預留(reserved)的編號在後續擴充套件也不能使用。對於預留(reserved)的後面將講到。
指定欄位規則
訊息(message)的欄位可以使用兩種規則描述(proto2與proto3不同):
-
單一的(singular):0個或1個,不用在欄位定義中指出。
-
重複的(repeated):0個到多個,需要在欄位定義中指出。 看如下例子:一個人,只有一個正式的名字(在剛出生的時候名字還沒登記),但是他可以有多個外號,也可以沒有。
syntax = "proto3"; message Person{ string name = 1; repeated string nickname = 2; } 複製程式碼
新增更多的訊息型別
多個訊息型別可以在一個.proto
檔案中定義。
比如在上面SearchRequest中新增一個SearchResponse。
message SearchRequest{
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse{
repeated string result = 1;
int32 page_number = 2;
}
複製程式碼
註釋
在.proto
檔案中註釋為C/C++風格,用//
註釋單方,或/**/
註釋多行。
預留(reserved)欄位
前面提到ProtocolBuffer自己預留(reserved)的欄位號19000~19999
。
可以自己預留欄位名或者欄位號,這樣預留(reserved)的欄位將不會被以後的使用者修改了。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
複製程式碼
編譯器編譯.proto
檔案
可以使用ProtocolBufer編譯器將.proto
檔案編譯成自己選擇的語言。在之後可以使用編譯後的訊息(message)進行get/set欄位值,序列化訊息(message)到輸出流,或者從輸入流中反序列化得到訊息(message)。
- C++:一個
.proto
檔案編譯生成一對.h
和.cc
檔案。 - Java:生成
.java
檔案,使用訊息(message)指定的Builder
類建立訊息(message)物件的例項。 - Python:Python有點不一樣:會生成一個module其中包含每個訊息(message)的靜態描述符。
- Go:每一個訊息(message)生成一個
.pb.go
檔案。 - Ruby:生成
.rb
檔案,是一個module
中包含各個訊息(message)。 - Objective-C:一個
.proto
檔案生成一對pbobjc
和pbobjc.m
檔案,每個訊息(message)對應一個class。 - C#:生成
.cs
檔案,每個訊息(message)對應一個class。 - Dart:生成
.pb.dart
檔案,每個訊息(message)對應一個class。
基礎型別
- double:
- float:
- int32:使用可變長編碼,如果使用的該欄位會有負數,效率將變低,這時最好使用sint32。
- int64:使用可變長編碼,如果使用的該欄位會有負數,效率將變低,這時最好使用sint64。
- uint32:使用可變長編碼。
- uint64:使用可變長編碼。
- sin32:使用可變長編碼,有符號整形,有負數時使用常規的int32更有效率。
- sint64:使用可變長編碼,有符號整形,有負數時使用常規的int64更有效率。
- fixed32:固定4個位元組,如果數字大於
2^28
比uint32更有效率。 - fixed64:固定8個位元組,如果數字大於
2^56
比uint64更有效率。 - sfixed32:固定4個位元組。
- sfixed64:固定8個位元組。
- bool:
- string:字串,必須使用UTF-8或者7位ASCII編碼格式。
- bytes:有任意的byte序列。 具體的proto中各個欄位型別對映到對應語言中時,見下圖:
預設值
如果一個訊息(message)被解析了,但是其中的欄位並沒有被賦值,那麼將會被設定為預設值。
- string:空串
- bytes:空的bytes序列
- bool:false
- 數字型別:0
- 列舉型別:預設值為列舉型別中定義的第一個值,也就是0
- 訊息型別(message):取決於所編譯的語言。 對於repeated,為空的list。
列舉
這裡還是以之前的查百度的例子來說,有了查詢關鍵字query,對於結果,你有可能不只是想要瀏覽一下WEB頁面,還行看看視訊、圖片、新聞啥的。那麼這樣定義:
syntax = "proto3";
message SearchRequest{
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Category{
option allow_alias = true;
//第一個值必須為0。
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
NEWS = 3;
PRODUCT = 4;
VIDEO = 5;
//啟用了別名,則可以賦同一個值
GENERAL = 0;
}
Category result_type = 4;
}
複製程式碼
使用enum
關鍵字定義列舉型別。
列舉常量數值必須在32bit
的整型中。使用負數賦值列舉常量效率低,不推薦。對於列舉常量,可以定義在訊息(message)中,也可定義在訊息(message)外。比如上面定義在SearchRequest
中的Category
,以SearchRequest.Category
的方式來複用。
預留(reserved)值
同樣的,對於訊息(message)中可以預留(reserved)欄位號,在列舉中,可以預留(reserved)值。 下面預留(reserved)了,值,名字。(2,15,9到11,40到最大值都不能後續使用)。
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}
複製程式碼
使用其他訊息(message)作為欄位
之前定義的SearchRespon
訊息(message):
message SearchResponse{
repeated string result = 1;
int32 page_number = 2;
}
複製程式碼
對於結果,我們只能獲取多個字串,讓他迴應的訊息功能更強大一點,我們定義一個Result
訊息(message):
message SearchResponse{
repeated Result result = 1;
int32 page_number = 2;
}
message Result{
string url = 1;
string title = 2;
repeated string snippets = 3;
}
複製程式碼
在SearchRespon
中,我們巢狀了一個Result訊息(message),Result
中有請求的地址url
,標題title
還有描述片段snippets
。
巢狀定義
接下來,我們再具體化URL:
message SearchResponse{
repeated Result result = 1;
int32 page_number = 2;
}
message Result{
URL url = 1;
string title = 2;
repeated string snippets = 3;
}
message URL{
enum Protocol{
HTTP = 0;
HTTPS = 1;
}
Protocol protocol = 1;
string domain = 2;
int32 port = 3;
string filepath = 4;
}
複製程式碼
對於SearchResponse
訊息(message)中返回的結果result
,在Result
中又有訊息(message)URL
,在URL
中我們具體到,使用的協議、域名、埠、請求檔案路徑。所以,訊息之間可以互相巢狀,定義更加複雜的訊息。
匯入
在Java中,或者其他語言,需要匯入其他以及寫好的包,在ProtocolBuffer中也是一樣,可以匯入先前定義好的.proto
檔案,使用其中定義的訊息(message)或者服務(service)。
在同一目錄下,我將寫好的URL
放入URL.proto
檔案中,在定義SearchResponse
訊息(message)中匯入該檔案:
import "URL.proto";
message Result{
URL url = 1;
string title = 2;
repeated string snippets = 3;
}
複製程式碼
這樣就可以複用更多的自定義訊息(message)了。
對於import
,只能匯入其後續指定的.proto
檔案中定義的訊息(message)或服務。比如有3個.proto檔案
:
/*file A.proto*/
syntax = "proto3";
message A{
}
/*file B.proto*/
syntax = "proto3";
import "A.proto";
message B{
A a = 1;
}
/*file C.proto*/
syntax = "proto3";
import "B.proto";
message C{
A a = 1;
}
複製程式碼
在這其中,C是看不到A的,只有在B中import public "A.proto"
,C才能看見A。
Any欄位型別
Any
欄位型別是Google自己對於Proto中型別的封裝,並提供一定特定方法。
如下定義一個Any
欄位,需要匯入Google提供的any.proto
ErrorStatus
訊息呼叫details
的get
方法時,返回的例項是com.google.protobuf.Any
,對於該型別提供了pack和unpack方法,如下:
class Any {
// 對於給定的訊息打包成Any型別,字首則是預設的:type.googleapis.com
public static Any pack(Message message);
// 對於給定的訊息打包成Any型別,字首則是typeUrlPrefix指定的
public static Any pack(Message message,
String typeUrlPrefix);
// 檢查該Any型別是否是給定clazz的訊息型別
public <T extends Message> boolean is(class<T> clazz);
// 給定clazz訊息型別,將Any型別拆包成指定的訊息型別,如果不匹配丟擲異常
public <T extends Message> T unpack(class<T> clazz)
throws InvalidProtocolBufferException;
}
複製程式碼
Any
欄位給了一定的靈活性,在傳遞訊息時不用指定特定的型別,可以在傳遞不同訊息中傳遞不同的型別,在接收端進行判斷即可。在傳輸時,底層還是被轉換為bytes
型別。
Oneof欄位型別
Oneof
型別如下定義。
oneof oneof_name {
int32 foo_int = 4;
string foo_string = 9;
...
}
複製程式碼
對於這個oneof
訊息型別,我們可以這樣理解,它類似與C語言中的union型別(聯合體),最後生成的Java程式碼是這樣的:
public enum OneofNameCase
implements com.google.protobuf.Internal.EnumLite {
FOO_INT(4),
FOO_STRING(9),
...
ONEOFNAME_NOT_SET(0);
...
};
複製程式碼
如果設定了oneof_name
訊息中的foo_int
欄位,那foo_string
就無效。同樣的,如果設定了foo_string
欄位,那麼foo_int
欄位就無效。在Oneof
型別的訊息中,只有一片共享記憶體,每次只有一個欄位被設定。
需要注意,Oneof
的訊息不能使用repeated
描述。
在Java中提供了一下方法進行輔助使用:
對於生成類中的列舉類:
- int getNumber(): 返回在
.proto
檔案中定義的索引值,如foo_int
則返回4。 - static OneofNameCase forNumber(int value): 返回使用索引值相應的物件,如果該物件未設定則返回null,如4則返回
foo_int
。 生成類中: - OneofNameCase getOneofNameCase(): 返回已經設定了的物件,如果都沒有被設定返回ONEOFNAME_NOT_SET。
生成類中的
Builder
: - Builder clearOneofName(): 清空所有設定。
Map欄位型別
使用這樣定義Map
型別:
map<key_type, value_type> map_field = N;
複製程式碼
key_type
:可以使用任何常規型別(int32或者string等),不能使用浮點數和bytes型別定義。value_type
:可以是任何型別,除了又是一個Map
。 和Oneof
同樣,使用Map
定義的欄位不可以是**repeated
**的。
包:package
對於.proto
檔案,可以使用包組織,package
欄位就是類似於Java中的Package
。
定義的計算CalculateMsg
訊息,在proto.Calculation資料夾下:
Calculation
。
最好使用package
和資料夾想對應,在Java中的習慣哈。
定義服務Service
在這裡我使用上面CalculationMsg
的訊息類,定義了其相應了服務,RPC(Remote Procedure Call)。
package Calculation;
import "Calculation/CalculatMsg.proto";
service Calculator{
rpc Calc( CalRequest ) returns (CalResponse){}
}
複製程式碼
使用Proto編譯器編譯上面的檔案,相應於選擇的語言將生成服務的介面(interface)和客戶端的stub。 可以使用Google提供的gRPC,也可以使用第三方的RPC框架。 這裡我給大家看看模仿grpc.io提供例子寫的計算服務: 對於服務端:
複寫編譯生成的gRPC介面類,實現之前定義的calc函式:獲取請求的需要計算方法,數值1和數值2,計算,然後放入輸出流中,最後OnComplete。 客戶端則先Build一個請求,阻塞呼叫獲取結果。對映到JSON
Proto3能夠轉換到JSON資料格式,其相應的資料型別對映如下: 如果Proto中某個欄位未設定,在JSON中就是null。
選項Option
.proto
檔案中可以使用option
欄位宣告特定選項。Opion不會影響整體訊息的定義,但是在特定的上下文中進行影響。
Option選項也是分級別的,有時候在外定義,則影響的是檔案級別,如:java_package
、java_multiple_files
、java_outer_classname
等,分別是:編譯後在哪個java包下,是否將.proto
檔案中不同訊息分成多個檔案,定義編譯後的java類名。
package
在tech.sylardaemon.Calculation
中,生成後的類名CalculatorProt
,java_generic_services為true則是生成gRPC相應的服務介面和客戶stub。
還可以自己定義option,是ProtoBuf的一種高階應用,這裡就略過了,有興趣的同學可以自己查檢視。
編譯器使用
編譯器的使用如下:
protoc --proto_path = IMPORT_PATH --Language_out = DST_DIR path/to/*.proto
複製程式碼
- --proto_path:該引數輸入的IMPORT_PATH是指定你要編譯的***.proto檔案中import指令中查詢的目錄。如果省略,則使用當前編譯器執行的目錄。也可以多次使用--proto_path指定多個匯入目錄。可以使用-I**縮短。
- --Language_out:可以提供一個或多個輸出目錄:
- --cpp_out:生成C ++程式碼的目的目錄
- --java_out:生成Java程式碼的目的目錄
- --python_out:生成Python程式碼的目的目錄
- --go_out:生成Go程式碼的目的目錄
- --ruby_out:生成Ruby程式碼的目的目錄
- --objc_out:生成Objective-C程式碼的目的目錄
- --csharp_out:生成C#程式碼的目的目錄
- --php_out:生成PHP程式碼的目的目錄
- path/to/*.proto:最後的則是將要被編譯的proto檔案路徑。
完
整篇就差不多完成了,最後還有一點考試,如果有錯誤或者缺了啥,歡迎提出,大概寒假有時間就來改改,大家一起進步xio習。