gRPC
是一个远程调用框架,使用Protobuf
做为信息的载体来完成客户端和服务端的数据传输。关于怎么定义Protobuf
消息、搭建gRPC
服务在之前的系列文章中都有提及,今天来说一下在使用gRPC
和Protobuf
的过程中怎么传递动态参数。
首先说明一下,这里所说的动态参数指的是在定义Protobuf
消息时还不能确定其具体内容的复合类型字段,简单的说就是消息里的这个字段我们想传一个类似JSON
对象、Map
字典、结构体等等这样的组合值,但是JSON
里有哪些字段、每个字段值是什么类型或者Map
字典键值的类型我们在定义消息时还无法确定(能确定就可以定义子消息嵌套进来了,不在本文的讨论范围内),把这样的Protobuf
消息字段叫做动态参数。
针对通过Protobuf
传递动态参数的需求,官方文档里并没有给出标准的解决方案,目前我所知道的能够通过bytes
、Map<string, string>
以及proto.Struct
这三种Protobuf
消息字段的类型实现,每种方式也都有自己的优势和劣处,如果你碰巧知道更好的实现方案,欢迎在评论里留言讨论。
下面我们就来看一下使用这三种消息字段的类型如何实现动态参数的传递。
使用bytes传递JSON对象参数
Protobuf
里的bytes
类型的字段编码成Go
代码后对应的是Go
里的字节切片[]byte
类型。所以我们可以把动态参数的字段类型定义成bytes
类型,这样客户端把JSON
对象传递到服务端后,服务端能直接对动态参数里包含的JSON
对象做解码操作,省去了一次从string
到[]byte
的类型转换。
举个例子来说,在下面的Protobuf
消息定义里info
字段的类型是bytes
rpc UpdateRecord (UpdateRecordRequest) returns (UpdateRecordReply) {
}
message UpdateRecordRequest {
int64 id = 1;
bytes info = 2;
}
那么在使用对于这个gRPC方法,客户端在使用的时候,直接把info
数据通过json.Marshal
编码后传递给服务端即可。
info := struct {
name string
age int
} {
name: "James",
age: 20,
}
jsonInfo, _ := json.Marshal(info)
_ := AppService.UpdateRecord(&AppService.UpdateRecordRequest{id: 2, info: jsonInfo})
在服务端可以加一个参数验证,保证传递过来的是一个正确的JSON
对象。
func IsJSON(in []byte) bool {
var js map[string]interface{}
return json.Unmarshal(in, &js) == nil
}
验证完后就可以根据实际的使用需求解码动态参数里的JSON
对象解析到具体的结构体变量。
type Info struct {
name string `json:"name"`
age int `json:"id"`
}
func (s server) UpdateRecord(ctx context.Context, reqeust *AppService.UpdateRecordRequest) (reply *AppService.UpdateRecordReply, err error) {
if !isJson(req.Info) {
// 错误处理
...
}
v := Info{}
json.Unmarshal(req.Info, $v)
}
我一般是这种方法,感觉比较方便,唯一算是麻烦的地方就是每个使用动态参数的地方要自己定义解析JSON
对象对应的结构体类型。
使用Map类型传递动态参数
如果你不想通过JSON
对象来传递参数,另一种经常能想到的方案是把参数的字段类型定义成字典,具体每次调用时可以根据需要设置不同的Key-Value
对。Protobuf
恰好也有Map
类型。
map<key_type, value_type> map_field = N;
但是有一点,在定义Map
类型时,值的类型必须是固定的,并不支持像map[string]interface{}
这样的值类型。所以这种方式一般是在能确定字典参数的值类型时使用,否则如果定义成了map<string, string>
的话假如要传递整型的字段,客户端还需要先将数据从整型转换成字符串类型。
使用proto.Struct传递结构体动态参数
有些资料里提到了使用Protobuf
里自带了一个复合类型proto.Struct
传递动态类型参数,使用它的好处是它看起来是Protobuf
对动态类型数据的一种原生支持,可以使用Protobuf
自带的包jsonpb
完成从JSON
到proto.Struct
之间的转换。
使用proto.Struct
类型需要在proto
文件里先引入它的类型定义,像下面这样。
syntax = "proto3";
package messages;
import "google/protobuf/struct.proto";
service UserService {
rpc SendJson (SendJsonRequest) returns (SendJsonResponse) {}
}
message SendJsonRequest {
string UserID = 1;
google.protobuf.Struct Details = 2;
}
message SendJsonResponse {
string Response = 1;
}
通过proto.Struct
的源码定义能看到它底层其实是一个名叫Struct
的消息,里面只包含了一个名叫fileds的Map
类型字段,通过Protobuf
的Oneof
特性指定了Map
值的类型范围来近似完成了动态类型的支持。
message Struct {
// Unordered map of dynamically typed values.
map<string, Value> fields = 1;
}
message Value {
// The kind of value.
oneof kind {
// Represents a null value.
NullValue null_value = 1;
// Represents a double value.
double number_value = 2;
// Represents a string value.
string string_value = 3;
// Represents a boolean value.
bool bool_value = 4;
// Represents a structured value.
Struct struct_value = 5;
// Represents a repeated `Value`.
ListValue list_value = 6;
}
}
所以在使用的时候操作proto.Struct
有点像操作字典,下面是一个使用的示例。
func sendJson(userClient pb.UserServiceClient, ctx context.Context) {
var item = &structpb.Struct{
Fields: map[string]*structpb.Value{
"name": &structpb.Value{
Kind: &structpb.Value_StringValue{
StringValue: "James",
},
},
"age": &structpb.Value{
Kind: &structpb.Value_NumberValue{
NumberValue: 20,
},
},
},
}
userGetRequest := &pb.SendJsonRequest{
UserID: "A123",
Details: item,
}
res, err := userClient.SendJson(ctx, userGetRequest)
}
总结
三种方法总结下来我还是觉得第一种使用起来更方便,第二种只能把值类型局限为一种,否则就需要在客户端和服务端做类型转换,第三种也是网上能找到对proto.Struct
的使用的资料较少,上手难度较大,且也不如第一种灵活。另外Protobuf
还有一个Any
类型,让我们使用的时候不需要定义消息,但是要携带一个说明数据的url
使用起来感觉也不太方便。这块如果读者朋友们相关经验可以一起来探讨一下。
最后做一个投票,针对gRPC动态参数这种需求大家都是怎么解决?