grpc 内置protobuf类型使用

介绍内置protobuf 类型使用过程中,使用 B站开源框架 kratos 作为示例demo,kratos 框架会同时生成http代码及rpc代码,方便演示。kratos详细了解

使用protocal buffers 定义传参类型时,经常会遇到复杂场景及一些特殊情况需要定义proto文件的message,本文介绍protocal buffers内置数据类型的使用场景及使用示例。由于个人水平有限,如有不当之处还请指正。

空请求 empty 类型

适用无传参或返回的message 类型,但通常不建议使用,为了更好的向后兼容,通常定义一个空的message。

proto定义

需要导入 google/protobuf/empty.proto 包。

1
2
3
4
5
6
7
import "google/protobuf/empty.proto";

rpc SayEmpty (google.protobuf.Empty) returns (HelloReply) {
option (google.api.http) = {
get: "/helloworld/{name}"
};
}

service 层使用方法,则需要导入 emptypb 包

1
2
3
4
5
6
7
import "google.golang.org/protobuf/types/known/emptypb"

....省略其他代码

func (s *GreeterService) SayEmpty(ctx context.Context, _ *emptypb.Empty) (*v1.HelloReply, error) {
return &v1.HelloReply{Message: "Hello empty"}, nil
}

空message表示,无传参或返回具体数据,但为了更好的兼容,通常不建议这样做。

wrappers 类型

用于区分参数为空及默认值场景。

protocal buffers 定义的message 中,参数值默认为类型的默认值,即:bool 类型默认为 false, int32,int64 默认为 0,string 默认为空字符串等等。

如何区分本身是默认值还是传参数为空呢?这里就需要用到wrappers包的相关类型。

proto 定义:

1
2
3
4
5
6
7
8
import "google/protobuf/wrappers.proto";

....省略其他代码

message HelloIsDefaultRequest {
string name = 1;
google.protobuf.Int32Value age = 2;
}

service 使用:

可通过值是否为 nil 来判断是否为默认值还是传参为类型的0值。

1
2
3
4
5
6
7
8
9
10
func (s *GreeterService) IsDefault(ctx context.Context, in *v1.HelloIsDefaultRequest) (*v1.HelloDefaultReply, error)  {
var age int32 = 18
if in.GetAge() != nil {
age = in.GetAge().GetValue()
}
return &v1.HelloDefaultReply{
Name: in.GetName(),
Age: age,
}, nil
}

wrappers包下可包装的类型有多个,有: Int32Value, Int64Value, BoolValue, ListValue, NullValue, StringValue, BytesValue等多种类型,具体了解,可查看代码。

FieldMask 类型

个人在使用过程中,主要用于更新方法,通过传参FieldMask 来控制更新字段

proto定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import "google/protobuf/field_mask.proto";

....省略其他代码

rpc FiledMask (HelloFieldMaskRequest) returns (HelloFieldMaskResponse) {
option (google.api.http) = {
post: "/hello/fieldMask"
};
}

....省略其他代码

message HelloFieldMaskRequest {
int32 task_id = 1;
bool is_delete = 2;
bool is_finished = 3;
google.protobuf.FieldMask field_mask = 4;
}

message HelloFieldMaskResponse {
repeated string field_mask = 1;
}

service 接收示例:

1
2
3
4
5
6
7
8
func (s *GreeterService) FiledMask(ctx context.Context, in *v1.HelloFieldMaskRequest) (*v1.HelloFieldMaskResponse, error)  {
s.log.Infof("task_id = %+v", in.GetTaskId())
s.log.Infof("is_finished = %+v", in.GetIsFinished())
s.log.Infof("is_deleted = %+v", in.GetIsDelete())
return &v1.HelloFieldMaskResponse{
FieldMask: in.GetFieldMask().Paths,
}, nil
}

此时接收到到FieldMask 字段为slice 类型,可判断字段是否在slice 里做响应逻辑处理。

请求示例:

1
curl -X POST -H "Content-Type:application/json" -d '{"task_id":1, "is_finished":true,"field_mask":"isFinished"}' 127.0.0.1:8000/hello/fieldMask

注意 传参为camelCase 类型,即 生成到pb文件里,tag为 protobuf json定义:isFinished, isDelete。

如:

1
2
3
4
5
6
7
8
9
10
type HelloFieldMaskRequest struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

TaskId int32 `protobuf:"varint,1,opt,name=task_id,json=taskId,proto3" json:"task_id,omitempty"`
IsDelete bool `protobuf:"varint,2,opt,name=is_delete,json=isDelete,proto3" json:"is_delete,omitempty"`
IsFinished bool `protobuf:"varint,3,opt,name=is_finished,json=isFinished,proto3" json:"is_finished,omitempty"`
FieldMask *fieldmaskpb.FieldMask `protobuf:"bytes,4,opt,name=field_mask,json=fieldMask,proto3" json:"field_mask,omitempty"`
}

另外如果field_mask传递多个字段,此时需要用“,” 分割:

如:

1
curl -X POST -H "Content-Type:application/json" -d '{"task_id":1, "is_finished":true,"field_mask":"isFinished, isDelete"}' 127.0.0.1:8000/hello/fieldMask

Any 类型

官方定义:

The Any message type lets you use messages as embedded types without having their .proto definition. An Any contains an arbitrary serialized message as bytes, along with a URL that acts as a globally unique identifier for and resolves to that message’s type. To use the Any type, you need to import google/protobuf/any.proto.

Any 消息类型允许您将消息用作嵌入类型,而无需它们的 .proto 定义。 Any 包含作为字节的任意序列化消息,以及充当全局唯一标识符并解析为该消息类型的 URL。 要使用 Any 类型,您需要导入 google/protobuf/any.proto。

如:

1
2
3
4
5
6
import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}

给定消息类型的 URL 定义格式为:

1
type.googleapis.com/_packagename_._messagename_.

使用示例:

proto 定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
syntax = "proto3";

package helloworld.v1;

import "google/protobuf/any.proto";

....省略其他代码

rpc AnyTypes (HelloAnyTypesRequest) returns (HelloAnyTypesResponse) {
option (google.api.http) = {
post: "/hello/any"
};
}

....省略其他代码

message HelloAnyTypesRequest {
string topic = 1;
google.protobuf.Any desc = 2;
}

message DescType {
string value = 1;
}

message HelloAnyTypesResponse {
string topic = 1;
string desc = 2;
}

service 使用示例:

1
2
3
4
5
6
7
8
func (s *GreeterService) AnyTypes(ctx context.Context, in *v1.HelloAnyTypesRequest) (*v1.HelloAnyTypesResponse, error)  {
s.log.Infof("topic = %+v", in.GetTopic())
s.log.Infof("desc = %s", in.GetDesc().GetValue())
return &v1.HelloAnyTypesResponse{
Topic: in.GetTopic(),
Desc: string(in.GetDesc().GetValue()),
}, nil
}

请求示例:

1
curl -X POST -H "Content-Type:application/json" -d '{"topic":"this is any type", "desc":{"@type":"type.googleapis.com/helloworld.v1.DescType", "value":"this is any type desc"}}' 127.0.0.1:8000/hello/any
  • @type 为 type.googleapis.com/ + package_name + message_name
  • value 为message 的字段定义

Timestamp 类型

传递 ISO 时间对象,接收到之后,通过proto可以任意 转换时间对象方便处理:

proto 定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
syntax = "proto3";

....省略其他代码

rpc Times (HelloTsRequest) returns (HelloTsResponse) {
option (google.api.http) = {
post: "/hello/ts"
};
}

....省略其他代码

message HelloTsRequest {
google.protobuf.Timestamp time_begin = 1;
}

message HelloTsResponse {
int64 timestamp = 1;
}

service 使用示例:

1
2
3
4
5
func (s *GreeterService) Times(ctx context.Context, in *v1.HelloTsRequest) (*v1.HelloTsResponse, error)  {
s.log.Infof("seconds = %+v", in.GetTimeBegin().GetSeconds())
s.log.Infof("nano = %s", in.GetTimeBegin().GetNanos())
return &v1.HelloTsResponse{Timestamp: in.GetTimeBegin().GetSeconds()},nil
}

调用示例:

1
curl -X POST -H "Content-Type:application/json" -d '{"time_begin":"2021-06-08T15:15:30.069Z"}' 127.0.0.1:8000/hello/ts

注意传递的时间对象为UTC标准时间,否则转化获取到的时间戳不正确。

javascript demo

Struct 类型

官方描述

Any JSON object

任意 json 类型,当我们传递不固定的json数据时,此时无法对应到某个具体到proto message, struct类型就大显神通了。

下面来看看具体到使用:

proto文件到定义:

需要导入 struct包:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
syntax = "proto3";

package helloworld.v1;

import "google/protobuf/struct.proto";

....省略其他代码

rpc AnyJson (HelloStructRequest) returns (HelloStructResponse) {
option (google.api.http) = {
post: "/hello/struct"
};
}

....省略其他代码

message HelloStructRequest {
google.protobuf.Struct json = 1;
}

message HelloStructResponse {
string detail = 2;
}

service 业务代码处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func (s *GreeterService) AnyJson(ctx context.Context, in *v1.HelloStructRequest) (*v1.HelloStructResponse, error)  {
maps := in.GetJson().AsMap()
s.log.Infof("this is map[string]interface{} = %+v", maps)
for key, value := range in.GetJson().GetFields() {
s.log.Infof("maps key = %+v", key)
switch value.Kind.(type) {
case *structpb.Value_NumberValue:
s.log.Infof("maps value is number = %+v", value.GetNumberValue())
case *structpb.Value_StringValue:
s.log.Infof("maps value is number = %+v", value.GetStringValue())
case *structpb.Value_BoolValue:
s.log.Infof("maps value is number = %+v", value.GetBoolValue())
default:
s.log.Infof("maps value is other type = %+v", value.AsInterface())
}
}
bts, _ := json.Marshal(maps)
return &v1.HelloStructResponse{
Detail: string(bts),
},nil
}

值的类型,可通过 structpb 包下定义的枚举类型来判断:structpb.Value_StringValue,Value_NumberValue, Value_BoolValue, structpb.Value_ListValue, structpb.Value_StructValue, structpb.Value_NullValue等等,具体枚举值,可查看structpb包源码。

代码地址

https://github.com/luckylsx/kratos-proto-demo

参考

Search by:GoogleBingBaidu