基于grpc从零开始搭建一个准生产分布式应用(4) - 01- proto详解

上一章节通过一个例子基本看到了如何使用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 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;
}

上述$$变量地自动替换成真实值。

你可能感兴趣的:(开发语言,DDD,系统架构,架构设计,spring,boot)