【微服务系列】Protocol buffer和grpc动态解析

17 篇文章 0 订阅
7 篇文章 0 订阅

最近的工作中用到了grpc。之前工作中使用的是基于thrift的微服务框架,对grpc不是很熟悉,只知道grpc是一个基于http2和protobuf的rpc框架。但是使用方法都是大同小异的,基于idl生成相应的文件,服务端的话就实现具体的service并对外提供服务,客户端的话需要引入client包发起rpc调用。

这里有个问题,就是要调用下游的服务需要引入下游的client,如果下游服务的idl发生变动的话我们需要在代码中更新client包,重新编译部署发布。在常规的业务开发中这种模式是没什么问题的,发布部署的节奏是跟随业务迭代的节奏来的。但是在某些场景下可能存在比较大的问题,比如一个网关服务,网关上注册了很多的业务,网关通过rpc来调用业务服务。在这种情况下如果每次业务变更idl都需要网关随着发布部署那显然是问题很大的。

针对上面的问题进行了调研,发现可以使用动态解析idl文件的方式来解决问题。可以使用github.com/jhump/protoreflect来实现pb和grpc的反射,即runtime时解析。。

pb reflection

代码示例

建立简单的proto文件如下,其中定义了简单的结构体HelloReq和HelloResp。

syntax = "proto3";
package test;

option go_package = "data/";

message HelloReq {
  int64 ID = 1;
  string Name = 2;
}

message HelloResp {
  int64 Code = 1;
  string Message = 2;
}

按照一般的做法,这时应该执行protoc --go_out=. ./test.proto命令生成test.pb.go,然后使用该结构体做相应的操作。

下面我们使用上面提到protoreflect包来进行运行时的解析,代码如下。

package main

import (
   "encoding/json"
   "github.com/jhump/protoreflect/desc/protoparse"
   "github.com/jhump/protoreflect/dynamic"
   "grpc_practice/data"
   "log"
)

func main() {
   // 创建parser对象
   p := protoparse.Parser{}
   // 使用path的方式解析得到一些列文件描述对象,这里只有一个文件描述对象
   fileDescs, err := p.ParseFiles("./test.proto")
   if err != nil {
      log.Printf("parse proto file failed, err = %s", err.Error())
      return
   }
   
   // 从文件描述对象中根据消息名拿到消息藐视对象
   helloReqDesc := fileDescs[0].FindMessage("test.HelloReq")
   if helloReqDesc == nil {
      log.Printf("no message matched")
      return
   }
   
   // 根据消息描述对象生成动态消息
   dMsg := dynamic.NewMessage(helloReqDesc)
   
   // 使用该动态对象从GetHelloReq模拟的json对象解码
   _ = dMsg.UnmarshalJSON(GetHelloReq())
   log.Printf("req ID = %d, req Name = %s", dMsg.GetFieldByNumber(1), dMsg.GetFieldByNumber(2))

}

func GetHelloReq() []byte {
   req := data.HelloReq{}
   req.ID = 1
   req.Name = "xiaoshuai"
   d, _ := json.Marshal(&req)
   return d
}

原理解析

上面的demo还是比较简单的,看上面的代码不难发现核心的对象有两个,一是descriptor描述符,其对应了proto文件中的不同层级的对象;二是dynamicMessage动态消息,其代替了pb.go文件中的消息体。

简单来说的话,descriptor就是解析proto文件,并以数据结构的形式组织持有proto文件中的信息。dynamicMessage持有MessageDescriptor对象,就相当于了解该结构体所有信息。

Descriptor

不同层级的描述符的关系如下,没什么好详细展开的。
在这里插入图片描述

dynamicMessage


// Message is a dynamic protobuf message. Instead of a generated struct,
// like most protobuf messages, this is a map of field number to values and
// a message descriptor, which is used to validate the field values and
// also to de-serialize messages (from the standard binary format, as well
// as from the text format and from JSON).
type Message struct {
   md            *desc.MessageDescriptor
   er            *ExtensionRegistry
   mf            *MessageFactory
   extraFields   map[int32]*desc.FieldDescriptor
   values        map[int32]interface{}
   unknownFields map[int32][]UnknownField
}

dynamic message的源码如下,看到其主要的对象就是message descriptor,持有md对象dynamic message就拥有一个消息所有的信息,比如字段名称、序号、类型等。然后其值是存放在values这个map字段中,其值是以interface{}存放,所以用的时候会有大量的反射,后面会补充一些benchmark,观测其性能。

下面是官方给的描述,觉得非常贴切,贴在这里。

The dynamic package provides a dynamic message implementation. It implements proto.Message but is backed by a message descriptor and a map of fields->values, instead of a generated struct. This is useful for acting generically with protocol buffer messages, without having to generate and link in Go code for every kind of message. This is particularly useful for general-purpose tools that need to operate on arbitrary protocol buffer schemas. This is made possible by having the tools load descriptors at runtime.

grpc reflection

代码示例

proto文件如下,定义了一个简单hello service,其持有一个bidirection stream方法和一个uniary方法。

syntax = "proto3";
package test;

option go_package = "data/";

message HelloReq {
  int64 ID = 1;
  string Name = 2;
}

message HelloResp {
  int64 Code = 1;
  string Message = 2;
}

service HelloService {
  rpc Hello (stream HelloReq) returns (stream HelloResp);
  rpc UniaryHello(HelloReq) returns (HelloResp);
}

下面我们使用上面提到protoreflect包来进行运行时的解析,实现client调用,代码如下。

package main

import (
   "context"
   "github.com/jhump/protoreflect/desc/protoparse"
   "github.com/jhump/protoreflect/dynamic"
   "github.com/jhump/protoreflect/dynamic/grpcdynamic"
   "google.golang.org/grpc"
   "log"
)

func main() {

   p := protoparse.Parser{}
   // 使用path的方式解析得到一些列文件描述对象,这里只有一个文件描述对象
   fileDescs, err := p.ParseFiles("./test.proto")
   if err != nil {
      log.Printf("parse proto file failed, err = %s", err.Error())
      return
   }

   // 从文件描述对象中根据消息名拿到消息描述对象
   helloReqDesc := fileDescs[0].FindMessage("test.HelloReq")
   if helloReqDesc == nil {
      log.Printf("no message matched")
      return
   }

   // 根据消息描述对象生成动态消息
   dMsg := dynamic.NewMessage(helloReqDesc)
   // 从service descriptor中拿到method descriptor
   helloMethodDesc := fileDescs[0].FindService("test.HelloService").FindMethodByName("test.UniaryHello")

   // 创建grpc连接
   conn, err := grpc.Dial("127.0.0.1:8080", grpc.WithInsecure())
   if err != nil {
      return
   }
   // 使用grpc连接创建动态的client
   client := grpcdynamic.NewStub(conn)

   // 调用方法
   resp, err := client.InvokeRpc(context.Background(), helloMethodDesc, dMsg)
   if err != nil {
      return
   }
}

原理解析

原理上和上面差不多,就不进行多余的解释了。


如果觉得本文对您有帮助,可以请博主喝杯咖啡~

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 8
    评论
评论 8
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值