介绍内置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
参考