開始前必讀:基于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;
}