前面我们说到了 gorpc 调用有反射和代码生成两种调用方式。前面的介绍一直都以反射的调用方式来进行介绍。下面我们先来介绍一下代码生成方式的调用全过程,接着介绍下代码生成工具的调研和具体实现。

一、以代码生成方式调用全过程。

  1. 定义一个服务,这里使用 proto 文件的形式进行定义
syntax = "proto3";

package helloworld;

service Greeter {
  rpc SayHello (HelloRequest) returns (HelloReply) {}
}

message HelloRequest {
  string msg = 1;
}

message HelloReply {
  string msg = 1;
}
  1. 安装 protoc-gen-gorpc 代码生成工具,在终端执行(前提是需要已经安装 protoc 工具,详情可参考protoc-gen-go 安装
go get -v github.com/lubanproj/protoc-gen-gorpc
  1. 执行
protoc --gorpc_out=plugin:. helloworld.proto

测试会生成 helloworld.pb.go 文件,这个文件里面包括 server 的描述文件和 client 的 stub 代码。

二、代码生成的文件内容

代码生成的文件内容主要有两块:

第一块主要是 server 服务信息,包括 GreeterService (服务的接口定义)、RegisterService(服务的注册函数)、 _Greeter_serviceDesc(服务名、方法名等描述信息) 、GreeterService_SayHello_Handler(方法的 Handler)等信息,如下:

// This following code was generated by protoc-gen-gorpc, DO NOT EDIT!!!

//================== server skeleton ===================
type GreeterService interface {
   SayHello(ctx context.Context, req *HelloRequest) (*HelloReply, error)
}

var _Greeter_serviceDesc = &gorpc.ServiceDesc{
   ServiceName: "helloworld.Greeter",
   HandlerType: (*GreeterService)(nil),
   Methods: []*gorpc.MethodDesc{

      {
         MethodName: "SayHello",
         Handler:    GreeterService_SayHello_Handler,
      },
   },
}

func GreeterService_SayHello_Handler(ctx context.Context, svr interface{}, dec func(interface{}) error, ceps []interceptor.ServerInterceptor) (interface{}, error) {

   req := new(HelloRequest)
   if err := dec(req); err != nil {
      return nil, err
   }

   if len(ceps) == 0 {
      return svr.(GreeterService).SayHello(ctx, req)
   }

   handler := func(ctx context.Context, reqbody interface{}) (interface{}, error) {
      return svr.(GreeterService).SayHello(ctx, reqbody.(*HelloRequest))
   }

   return interceptor.ServerIntercept(ctx, req, ceps, handler)
}

func RegisterService(s *gorpc.Server, svr interface{}) {
   s.Register(_Greeter_serviceDesc, svr)
}

第二块主要包括 client 桩代码,主要生成了一个 proxy 代理 GreeterClientProxy ,以及代理的实现 GreeterClientProxyImpl,GreeterClientProxyImpl 实现了 GreeterClientProxy 接口定义的 SayHello 方法。

//================== client stub===================
//GreeterClientProxy is a client proxy for service Greeter.
type GreeterClientProxy interface {
   SayHello(ctx context.Context, req *HelloRequest, opts ...client.Option) (*HelloReply, error)
}

type GreeterClientProxyImpl struct {
   client client.Client
   opts   []client.Option
}

func NewGreeterClientProxy(opts ...client.Option) GreeterClientProxy {
   return &GreeterClientProxyImpl{client: client.DefaultClient, opts: opts}
}

// SayHello is server rpc method as defined
func (c *GreeterClientProxyImpl) SayHello(ctx context.Context, req *HelloRequest, opts ...client.Option) (*HelloReply, error) {

   callopts := make([]client.Option, 0, len(c.opts)+len(opts))
   callopts = append(callopts, c.opts...)
   callopts = append(callopts, opts...)

   rsp := &HelloReply{}
   err := c.client.Invoke(ctx, req, rsp, "/helloworld.Greeter/SayHello", callopts...)
   if err != nil {
      return nil, err
   }

   return rsp, nil
}

三、为什么需要代码生成?

我们知道,server 收到 client 请求后,需要调用相应的 Handler 去处理,通过反射的方式去调用服务,Handler 里面的服务名、方法名等信息需要通过 reflect 包去动态获取,这个过程是有不小性能损耗的(下一章节会介绍)。假如不 care 这点性能损耗,又比较喜欢通过 go struct 的形式定义服务,当然可以采用这种方式。假如 care 性能损耗的话,就可以使用代码生成的方式,通过代码生成工具,将 proto 文件里面定义的 Service 生成相应的 server handler 和 client stub 代码。服务名、方法名、Handler 等信息提前静态生成好,就能避免反射带来的性能损耗。

四、代码生成工具实现思路

要基于 protoc 实现代码生成工具,首先需要去了解 protoc 代码生成的原理,如下图(图片来源于网络):

img

从上图可以看到,protoc 代码生成主要分为五个步骤,我们只需要关注第三个步骤——插件的实现即可。

protoc 工具为了支持 go 语言的代码生成,提供了 protoc-gen-go 插件,在执行 protoc 命令时加上 –go_out 这个选项,protoc 就会默认去找 protoc-gen-go 这个插件,既然是关注插件的实现,那么我们有两种方式可以做到:

1、模仿 grpc 的方式

由于 grpc 和 protobuf 都是 google 的项目,google 默认在 protoc-gen-go 这个项目下有一个 grpc 目录去负责 grpc 的代码生成,参考:protoc-gen-go

使用 protoc 生成 grpc 的调用代码命令如下:

protoc --go_out=plugins=grpc:. --go_opt=paths=source_relative helloworld/helloworld.proto

可以看到 protoc 也是使用 –go_out=plugins=grpc 这个参数选项,传入 plugins 这个参数,当发现 plugins 这个参数时,protoc 会去找实现了 Plugin 接口的插件, protoc-gen-go 项目的 grpc 包刚好实现了 Plugin 插件,所以就会执行 grpc.go 中的代码,生成 grpc 的调用代码 grpc.go

假如需要模仿 grpc,那么我们需要下载 protoc-gen-go 的包,然后建立一个与 grpc 同目录的包 gorpc,实现 Plugin 插件,代码生成时采用 protoc –go_out=plugins=gorpc:. 这种方式。

2、制作 protoc-gen-gorpc 插件

第二种方式是使用 protoc –gorpc_out=:. 这个命令,这个命令会去你的系统 Path 路径去寻找 protoc-gen-gorpc 的 bin 文件去执行代码生成,相比于第一种方式而言,这种方式更加灵活。

考虑到灵活性,我们选择第二种方式进行代码生成。

五、代码生成工具实现细节

1、main 函数的编写

main 函数的编写主要是实现了上图的第三个步骤,从 os.Stdin 中读取二进制数据,然后反序列化成 CodeGeneratorRequest 对象,然后解析 CodeGeneratorRequest ,根据需求构造 CodeGeneratorResponse 对象,将 CodeGeneratorResponse 对象序列化成二进制数据,通过 os.Stout 返回给 protoc 插件。这里的代码目前所有社区的 protoc 插件都可以通用,直接 copy 即可:main.go

2、实现 Plugin

上面说到了,要根据 protoc 制作一个代码生成插件,需要实现 Plugin 接口,我们先来看看 Plugin 接口如下:

// A Plugin provides functionality to add to the output during Go code generation,
// such as to produce RPC stubs.
type Plugin interface {
   // Name identifies the plugin.
   Name() string
   // Init is called once after data structures are built but before
   // code generation begins.
   Init(g *Generator)
   // Generate produces the code generated by the plugin for this file,
   // except for the imports, by calling the generator's methods P, In, and Out.
   Generate(file *FileDescriptor)
   // GenerateImports produces the import declarations for this file.
   // It is called after Generate.
   GenerateImports(file *FileDescriptor)
}

我们主要是实现 Generate 方法即可,在 main.go 的 g.GenerateAllFiles() ——> g.generate(file) ——> g.runPlugins(file) 里面会去执行所有实现了 Plugin 接口插件的代码,主要是调用 Generate() 方法,如下:

// Run all the plugins associated with the file.
func (g *Generator) runPlugins(file *FileDescriptor) {
   gorpclog.Debugf("plugins : %v", plugins)
   gorpclog.Debugf("len(plugins) : %d", len(plugins))
   for _, p := range plugins {
      p.Generate(file)
   }
}

3、实现 Generate

gorpc 的 Generate 方法主要是生成 import 和针对每个 proto 文件定义的 Service,去生成具体 Service 代码

// Generate generates code for the services in the given file.
func (p *gorpc) Generate(file *generator.FileDescriptor) {

   if len(file.FileDescriptorProto.Service) == 0 {
      return
   }
   _ = p.gen.AddImport(gorpcServerPkgPath)
   _ = p.gen.AddImport(gorpcClientPkgPath)
   _ = p.gen.AddImport(gorpcInterceptorPkgPath)
   _ = p.gen.AddImport("context")

   // generate all services
   for i, service := range file.FileDescriptorProto.Service {
      p.generateService(file, service, i)
   }
}

4、实现 generateService

下面这段代码实现了 gorpc 的所有需要的 server 代码 和 client stub 代码的生成,它的核心原理主要是调用 p.P 去生成静态代码,通过 generator.FileDescriptor 、pb.ServiceDescriptorProto 这两个对象,获得 proto 文件定义的服务信息,然后生成相应的静态代码,这里就不过多赘述,读者直接读源码即可。

// generateService generates all the code for the named service
func (p *gorpc) generateService(file *generator.FileDescriptor, service *pb.ServiceDescriptorProto, index int) {
   originServiceName := service.GetName()
   serviceName := upperFirstLatter(originServiceName)
   p.P("// This following code was generated by protoc-gen-gorpc, DO NOT EDIT!!!")
   p.P()
   p.P("//================== server skeleton ===================")
   p.P(fmt.Sprintf(`type %sService interface {
            `, serviceName))

   for _, method := range service.Method {
      p.generateServerInterfaceCode(method)
   }
   p.P("}")

   p.generateServiceDesc(service, file.GetPackage())
   for _, method := range service.Method {
      p.generateServerCode(service, method, file.GetPackage())
   }
   p.generateRegisterCode(service)

   p.P("//================== client stub===================")
   p.P(fmt.Sprintf(`//%sClientProxy is a client proxy for service %s.
      type %sClientProxy interface {
   `, serviceName, serviceName, serviceName))
   for _, method := range service.Method {
      p.generateClientInterfaceCode(method)
   }
   p.P("}")
   p.P(fmt.Sprintf(`
      type %sClientProxyImpl struct {
         client client.Client
         opts   []client.Option
      }
   `, serviceName))
   p.P(fmt.Sprintf(`
      func New%sClientProxy(opts ...client.Option) %sClientProxy {
         return &%sClientProxyImpl{client: client.DefaultClient, opts: opts}
      }
   `, serviceName, serviceName, serviceName))

   for _, method := range service.Method {
      p.generateClientCode(service, method, file.GetPackage())
   }
}

六、小结

本章节主要介绍了 gorpc 代码生成工具的原理和实现,源码详见:protoc-gen-gorpc