gRPC 扩展类型的使用

gRPC 使用 protobuf 格式对消息进行编码, 基本类型都会映射到各种语言的类型. 为了丰富表达能力, 官方基于基本类型封装了一些类型, 例如: Timestamp, Duration, Any, Struct.
Timestamp⌗
Timestamp 类型是对时间戳的扩展, 因为字符串时间标准非常多而且不同语言差异很大, 此扩展格式兼顾了精度.
为了方便使用, 各语言基本都会有 Timestamp 和 Date(Time) 类型互相转换的函数.
源码方面也是非常简单, 基本都是处理精度转换, Golang 时间戳精度有 nanosecond 所以不需要转换, JavaScript 的时间戳为 13 位也就是精度是 Millisecond.
Duration⌗
Duration 类型很多语言并没有对应的格式, 不同语言时间精度也是不一样的, 所以此扩展也兼顾了精度.
格式和 Timestamp 完全相同, 只是表示的含义不同. 本质其实是提供了最高精度为 nanosecond 的持续时间 duration = seconds * 1e9 + nanos .
对于 Golang 这种自带 Duration 类型的语言, 提供了互转 API, 并会检查是否溢出:
而对于 JavaScript 这种没有对应类型的语言则没有任何转换方法, 需要开发者手动处理.
Any⌗
Any 类型允许我们使用此字段传递任何 Protocol Buffer 类型的消息, 类似于某些编程语言中的泛型.
只有两个字段, value 字段为消息通过 protobuf 序列化成 binary 之后的值, 而 type_url 则是该类型的 唯一标识符.
proto 文件定义的每种类型和方法都会有一个全局唯一标识符, 类型一般为 <package>.<type> 而 rpc 方法一般为 <package>.<service>/<method>. 后者查看 grpc client 生成文件可以看出 client 方法都是通过 c.cc.Invoke(ctx, "/pb.Hello/Echo", in, out, opts...) 这种形式执行调用逻辑的, 外层仅仅是生成了类型. 生成类型都会被保存在运行时的全局变量中, 运行时可以通过标识符或者 url 来查找, Golang 默认为 protoregistry.GlobalTypes.
之所以有 type_url 这个字段, 是因为一般来说 protobuf 消息序列化反序列化都需要类型定义, 因此有了类型标识符, 接收方就知道该反序列化成哪种类型的消息了. 这个字段最终还需要加上 type.googleapis.com/ 前缀.
Golang 生成消息类型可以使用反射获取到标识符:
Golang Any 类型提供了几个常用的方法:
以 UnmarshalNew 来举例(any.UnmarshalNew 就是单纯调用的 UnmarshalNew):
对于 JavaScript 这种动态语言来说, 使用起来就非常麻烦了, Any 类型仅提供了非常抽象的两个方法:
对于不熟悉 grpc 的用户来说根本不知道这两个函数该传什么参数进去, 这里必须要再次吐槽下, js grpc 社区基本没有文档, 很多时候我都是对比 golang 的表现去找源码, 但是很多时候你会发现很多 golang 实现了的它又是缺失的.
pack 方法基本等于直接调用 setTypeUrl 和 setValue 两个方法. 更不可思议的是, js protobuf 没有像 Golang 运行时获取消息 fullName 的 API. unpack 需要我们指定目标消息的反序列化函数, 也就是目标消息类型的 deserializeBinary 方法, 并且会在反序列化前比较传入的 name 和 any 消息的 type_url.
所以对于上面 go 语言的例子, 我们只能这么做:
可以看到 API 非常底层, 但是之前提到过消息类型会被保存在运行时的全局变量中, js protobuf 保存的地方就是 global.proto, 所以我们可以通过 global.proto.pb.EchoRequest 拿到 EchoRequest 的反序列化方法, 进而可以构造出一个类似于 Golang 的 UnmarshalNew 的动态反序列化方法:
Struct⌗
Struct 类型基本就是一个最外层不能是数组的动态 JSON 类型, 序列化反序列化都是通过运行时反射得到的字段类型来处理.
Golang 提供了 Struct 到 map[string]interface{} 的互转 API:
源码方面也是和解析 JSON 几乎一样, 都是通过获取每个字段的值类型, 设置成对应的 protobuf 类型:
而 JavaScript 这边也是提供了两个互转 API fromJavaScript 和 toJavaScript.
总的来说这种方式和使用 bytes 格式传递手动 JSON 序列化的消息, 接收方收到后手动 JSON 反序列化差不多.
总结⌗
上面介绍的这几种类型应该都是 Google 从自己生产需求中总结出来的并且被多种语言广泛使用的类型, 也为我们自己扩展通用消息类型做了示范. 可以看到这种多语言类型扩展做到贴合各自语言特性并且 API 设计人性化还是非常难的, 上文中 Any 类型对于 js 用户体验就很不好.
参考资料⌗
