Proto3入門

LudwigWuuu發表於2018-12-07

Proto3入門

本文基於Google提供的ProtolBuffer LanguageGuide英文文件: ProtolBuffer3 Language Guide

ProtoBuf的API文件

定義一個message

首先以一個簡單的例子開頭:比如查百度,那麼需要一個查詢語句:query,還有查詢的頁面號:page_number,然後就是查詢的每一頁的結果數:result_per_page。 這樣就有三個欄位:querypage_numberresult_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檔案生成一對pbobjcpbobjc.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中各個欄位型別對映到對應語言中時,見下圖:
    Proto3入門

預設值

如果一個訊息(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中又有訊息(messageURL,在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

Proto3入門
在Java中使用ErrorStatus訊息呼叫detailsget方法時,返回的例項是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資料夾下:

Proto3入門
其中的包就是Calculation
Proto3入門
最好使用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提供例子寫的計算服務: 對於服務端:

Proto3入門
複寫編譯生成的gRPC介面類,實現之前定義的calc函式:獲取請求的需要計算方法,數值1和數值2,計算,然後放入輸出流中,最後OnComplete。
Proto3入門
客戶端則先Build一個請求,阻塞呼叫獲取結果。

對映到JSON

Proto3能夠轉換到JSON資料格式,其相應的資料型別對映如下: 如果Proto中某個欄位未設定,在JSON中就是null。

Proto3入門

選項Option

.proto檔案中可以使用option欄位宣告特定選項。Opion不會影響整體訊息的定義,但是在特定的上下文中進行影響。 Option選項也是分級別的,有時候在外定義,則影響的是檔案級別,如:java_packagejava_multiple_filesjava_outer_classname等,分別是:編譯後在哪個java包下,是否將.proto檔案中不同訊息分成多個檔案,定義編譯後的java類名。

Proto3入門
之前定義的計算服務就是如上,生成的packagetech.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習。