天天看點

基于grpc從零開始搭建一個準生産分布式應用(4) - 01- proto詳解

開始前必讀:​基于grpc從零開始搭建一個準生産分布式應用(0) - quickStart​​ 

上一章節通過一個例子基本看到了如何使用proto來定義接口,本章就把proto的所有重要内容詳細講解下,内容不多也不難。在某些場景下還是需要一些使用技巧的,在本章的最後筆者會把使用過程中的一些坑分享一下,希望對您有所幫助。PS:proto3和proto2不是完全相容的,建議用proto3。

概述:proto完整定義

開始正文之前先看一下proto的完整定義,整個檔案沒有必填項,在下面源碼的注釋中筆者按使用經驗辨別出了哪些建議一定要寫,大體分為三部分:消息頭、接口定義、對象定義。完整定義如下:

/*========消息頭定義========*/
//可選配置,建議必配,協定,這塊與最終的編譯相關,現建議采用proto3标準
syntax = "proto3";

//可選配置,建議必配,全名空間,隻是proto檔案的命名空間,與最終生成的源碼無關
package com.zd.baseframework.core.sysrecord;

//可選配置,引入的擴充定義,最常用的就是下面這兩個了,如果文中沒有使用還是建議不引入
import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

//可選配置,建議必配,這三個配置與生成的源碼相關
option java_package = "com.zd.baseframework.core.sysrecord.api";
option java_outer_classname = "SysRecordProto";
option java_multiple_files = true;

/*========接口定義========*/
//可選配置,接口定義
service ISysRecordService{
  //建立
  rpc CreateSysRecord (CreateSysRecordRequest) returns (SysRecordOperatorResponse);
  //查詢
  rpc ListSysRecordByCondition (ListSysRecordRequest) returns (ListSysRecordResponse);

}

/*========對象定義========*/
//可選配置,接口出入參定義
message CreateSysRecordRequest{
  google.protobuf.StringValue biz_id = 1;
  google.protobuf.Int64Value user_id = 2;
  google.protobuf.StringValue track_uid = 3;
  google.protobuf.StringValue code = 4;
  google.protobuf.StringValue custom_code = 5;
}

message ListSysRecordRequest{
  google.protobuf.StringValue biz_id = 1;
  google.protobuf.Int64Value user_id = 2;
  google.protobuf.Int32Value code = 3;
}

message  SysRecordOperatorResponse{
  optional int32 status = 1;
  optional string message = 2;
}

message  ListSysRecordResponse{
  repeated SysRecordDto data = 1;
}

//資料傳輸對象,建議單獨定義
message SysRecordDto {
  int64 id = 1;
  int64 biz_id = 2;
  int64 user_id = 3;
  string track_uid = 4;
  string code = 5;
  string custom_code = 6;
  int32 state = 7;
  google.protobuf.Timestamp code_name = 8;
  google.protobuf.Timestamp utime = 9;
}      

好,了解完了整體定義,筆者就分幾部分詳細講解下各部分的可選擇配置有哪些,供讀者在實際開發中使用。

一、消息頭定義

以下四個一般是必須要寫的:

  • java_package (檔案選項) :表明生成java類所在的包。例:option java_package = "com.example.foo";​
  • java_outer_classname (檔案選項): 表明想要生成Java類的名稱。例:option java_outer_classname = "Ponycopter";
  • java_multiple_files (檔案選項):定義在最外層的 message 、enum、service 将作為單獨的類存在。例:option java_multiple_files = true,預設為flase。
  • package(檔案選項):用來防止不同的消息類型有命名沖突,例:package foo.bar;

以個是可選的:

  • deprecated(字段選項):如果設定為true則表示該字段已經被廢棄。例:int32 old_field = 6 [deprecated=true];

1.1、協定定義

syntax = "proto3"; //指定文法行必須是檔案的非空非注釋的第一個行,預設是proto2      

1.2、導入定義

注意:雖說proto有類似java的extends能力,但不建議這麼來做。因為消息的定義是用索引來區分的,後續如果改動了base定義很容易出現問題。是以在備援代碼和繼承能力間建議大家選擇備援實作。

//帶public的導入可以傳導,比如abc,a import b, b import c,這時a不能直接使用c,如果 b import public c則是可以使用的
import  "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";      

完整定義如下例所示,建議下面的格式做成項目組的一個開發規範:

syntax = "proto3";

package com.zd.baseframework.core.sysrecord;

import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

option java_package = "com.zd.baseframework.core.sysrecord.api";
option java_outer_classname = "SysRecordProto";
option java_multiple_files = true;      

二、消息定義

每個字段由字段限制、字段類型、字段名和編号四部分組成,消息中的每一個字段都有一個獨一無二的數值類型的編号。1到15使用一個位元組編碼,16到2047使用2個位元組編碼,是以應該将編号1到15留給頻繁使用的字段。

2.1、消息格式

普通消息,建議使用

syntax = "proto3";
 
message SearchRequest {
  string query = 1;
}      

 嵌套類型,不建議使用,原因是可讀性差,且源碼複雜

//嵌套類型
message SearchResponse {
  message Result {
    required string url = 1;
    optional string title = 2;
    repeated string snippets = 3;
  }
  repeated Result result = 1;
}

message SomeOtherMessage {
  optional SearchResponse.Result result = 1;
}      

2.2、消息配置

2.2.1、設定預設值

不建議使用,而且在proto3中也不支援預設值設定了。

int32 result_per_page = 3 [default = 10];      

2.2.2、字段修飾符

  • singular: 可以有0個或者1個這種字段(但是不能超過1個)。不建議使用;
  • required: 必須指派的字段,不建議使用,可由服務接口來程式設計實作;
  • optional: 可有可無的字段,不建議使用,可由服務接口來程式設計實作;
  • repeated: 可重複,相當于java中的List;

2.2.3、預留字段

因筆者沒用過這個設定,如果消息的字段被移除或注釋掉,但是使用者可能重複使用字段編碼,就有可能導緻例如資料損壞、隐私漏洞等問題。一種避免此類問題的方法就是指明這些删除的字段是保留的。如果有使用者使用這些字段的編号,protocol buffer編譯器會發出告警。

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

2.3、字段類型

2.3.1、基礎類型

在初始化時,都會帶有預設值。如果沒有指定預設值,則會使用系統預設值,對于string預設值為空字元串,對于bool預設值為false,對于數值類型預設值為0,對于enum預設值為定義中的第一個元素,對于repeated預設值為空。

proto Type Notes Java Type Python Type Go Type
double double float float64
float float float float32
int32 使用變長編碼,對于負值的效率很低,如果你的域有可能有負值,請使用sint64替代 int int int32
uint32 使用變長編碼 int int/long uint32
uint64 使用變長編碼 long int/long uint64
sint32 使用變長編碼,這些編碼在負值時比int32高效的多 int int int32
sint64 使用變長編碼,有符号的整型值。編碼時比通常的int64高效。 long int/long int64
fixed32 總是4個位元組,如果數值總是比總是比228大的話,這個類型會比uint32高效。 int int uint32
fixed64 總是8個位元組,如果數值總是比總是比256大的話,這個類型會比uint64高效。 long int/long uint64
sfixed32 總是4個位元組 int int int32
sfixed64 總是8個位元組 long int/long int64
bool boolean bool bool
string 一個字元串必須是UTF-8編碼或者7-bit ASCII編碼的文本。 String str/unicode string
bytes 可能包含任意順序的位元組資料。 ByteString str []byte

2.3.2、Google擴充的類型

以下的這些頭型不一定能用,但可以依照自定義實作:

//以下的這些頭型不一定能用,但可以自定義
calendar_period.proto
color.proto
date.proto
datetime.proto
dayofweek.proto
expr.proto
fraction.proto
latlng.proto
money.proto
postal_address.proto
quaternion.proto
timeofday.proto
http_request.proto      

以下三種是經常可以用到的:

wrappers.proto //各種基礎類型的包裝類型
timestamp.proto 
annotations.proto//注解      

三、複雜類型

3.1、list

//這處用repeated關鍵字來實作的
repeated Order ordersList = 3;      

3.2、map

key_type可以是除浮點指針或bytes外的其他基本類型,value_type可以是任意類型

map<string, Project> projects = 3;      

3.3、enum

不太建議使用

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 result_per_page = 3;
  enum Corpus {
    UNIVERSAL = 0; #每個枚舉類型必須将其第一個類型映射為0
    WEB = 1;
    IMAGES = 2;
    LOCAL = 3;
    NEWS = 4;
    PRODUCTS = 5;
    VIDEO = 6;
  }
  Corpus corpus = 4;
}      

設定可選參數allow_alias為true,就可以在枚舉結構中使用别名(兩個值元素值相同)

enum EnumAllowingAlias {
  option allow_alias = true;
  UNKNOWN = 0;
  STARTED = 1;
  RUNNING = 1;
}      

3.4、any

不太建議使用,因為在程式實作時返參判斷特别麻煩。Any可以讓你在 proto 檔案中使用未定義的類型,具體裡面儲存什麼資料,是在上層業務代碼使用的時候決定的,使用 Any 必須導入 import google/protobuf/any.proto

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

3.5、oneof

不太建議使用,原因同any。Oneof 類似union,如果你的消息中有很多可選字段,而同一個時刻最多僅有其中的一個字段被設定的話,你可以使用oneof來強化這個特性并且節約存儲空間。

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

四、服務定義

如果想要将消息類型用在RPC(遠端方法調用)系統中,可以在.proto檔案中定義一個RPC服務接口,protocol buffer編譯器将會根據所選擇的不同語言生成服務接口代碼及存根。如,想要定義一個RPC服務并具有一個方法,該方法能夠接收 SearchRequest并傳回一個SearchResponse,此時可以在.proto檔案中進行如下定義:

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

流stream接口定義,對應GRPC的流式通路,對于處理大流量資料時非常有用,詳細如下:

阻塞型定義 RPC:rpc Search (SearchRequest) returns (SearchResponse);
伺服器端流式 RPC:rpc ListFeatures(Rectangle) returns (stream Feature) 
用戶端流式 RPC:rpc RecordRoute(stream Point) returns (RouteSummary)
雙向流式 RPC:rpc RouteChat(stream RouteNote) returns (stream RouteNote)      

五、源碼生成

  • 生成C++源碼時:編譯器會為每個.proto檔案生成一個.h檔案和一個.cc檔案,.proto檔案中的每一個消息有一個對應的類;
  • 生成Java源碼時:編譯器為每一個消息類型生成了一個.java檔案,以及一個特殊的Builder類(該類是用來建立消息類接口的);
  • 生成Python源碼時:proto檔案中的每個消息類型生成一個含有靜态描述符的子產品,該子產品與一個元類(metaclass)在運作時(runtime)被用來建立所需的Python資料通路類;
  • 生成Go源碼時:編譯器會位每個消息類型生成了一個.pd.go檔案;
  • 生成javaNano源碼時:編譯器輸出類似域java但是沒有Builder類。

六、實踐經驗

6.1、proto對象列印

在java中有時需要把proto對象轉為json字元串列印日志,但必須用Google的工具類才可以,代碼如下:

String json = JsonFormat.printer().print(dtos);//格式化輸出
String json = JsonFormat.printer().omittingInsignificantWhitespace().print(dtos);//去掉換行      

6.2、JSON互轉

需要依賴com.googlecode.protobuf-java-format和protobuf-java-format包。代碼如下:

//protobuf對象轉換成json:
String jsonFormat = JsonFormat.printToString(SomeProto);

//json轉成protobuf對象:
Message.Builder builder =SomeProto.newBuilder();
String jsonFormat = "json字元串";
JsonFormat.merge(jsonFormat, builder);      

6.3、如何選擇包裝類型和基礎類型

建議定義接口入參時選擇包裝類型,這樣可以減少由于基本類型預設值問題帶來的莫名其妙的錯誤。因為包裝類型的預設值全是null;

建議定義接口出參時選擇基礎類型,因為結果傳回相對來說比較好控制,也更容易維護服務的嚴謹和穩定性。

6.4、proto定義快捷鍵盤

proto檔案的内容還是比較多的,一行行手寫比較慢,複制又容易出錯。是以這裡可以使用IDEA的Live Template功能定義一個子產品,下面是筆者寫的一個子產品,建立好proto檔案後,在空白地方手動輸入:proto,再按tab就會自動生成想要的子產品了。模闆定義如下:

syntax = "proto3";

package universe.core.$fileName$;

import "google/protobuf/timestamp.proto";
import "google/protobuf/wrappers.proto";

option java_package = "net.shukun.universe.core.$fileName$.api";
option java_outer_classname = "$camefileName$Proto";
option java_multiple_files = true;

service I$camefileName$Service{
  rpc ReProcess (Request) returns (Response);
}

message Request{
  google.protobuf.Int64Value org_id = 1;
  google.protobuf.StringValue workflow = 2;
  google.protobuf.StringValue case_num = 3;
}

//傳回值
message Response{
  optional int32 status = 1;
  optional string message = 2;
}