protobuf在gRPC中的应用

1,071 阅读1分钟

protobuf在gRPC中的应用

前面已经介绍了protobuf,这里我们先介绍了gRPC,了解gRPC前先了解下什么是RPC

RPC

RPC也就是remote procedure call翻译过来就是远程过程调用,RPC是一种概念,grpc实现rpc的一种框架

RPC流程图

  • stub是桩代码的意思,可以理解为存根
  • client stub:存放服务端的地址消息,再将客户端的请求参数打包成网络消息,然后通过网络远程发送给服务方。
  • server stub:接收客户端发送过来的消息,将消息解包,并调用本地的方法。

RPC需要解决的三个问题

  • 通信问题
  • 序列化和反序列问题
  • call ID 映射问题

protobuf在gRPC中的应用

protobuf在gRPC中的角色的接口定义语言,即解决的是序列化和反序列化的问题,而对于protobu来说,gprc只是生成service代码的一种工具

定义接口

  • 新建hello.proto
syntax = "proto3";
option go_package="./;hello";
service HelloService {
    rpc SayHello (HelloRequest) returns (HelloResponse);
}
  
  message HelloRequest {
    string greeting = 1;
}
  
  message HelloResponse {
    string reply = 1;
}

生成接口代码

  • 运行bash命令

    $ protoc --go_out=plugins=grpc:. .\hello.proto
    
  • 得到hello.pb.go文件

调用接口

server代码

var (
    port = flag.Int("port", 50051, "The server port")
)
​
type server struct {
    // can be embedded to have forward compatible implementations.
    helloworld.UnimplementedGreeterServer
}
​
func (s *server) SayHello(ctx context.Context, in *helloworld.HelloRequest) (*helloworld.HelloReply, error) {
    log.Printf("Received:%v", in.GetName())
    return &helloworld.HelloReply{Message: "Hello" + in.GetName()}, nil
}
​
func main() {
    flag.Parse()
    lis, err := net.Listen("tcp", fmt.Sprintf(":%d", *port))
    if err != nil {
        log.Fatalf("failed to listen:%v", err)
    }
​
    s := grpc.NewServer()
    helloworld.RegisterGreeterServer(s, &server{})
    log.Printf("server listening at %v", lis.Addr())
    if err := s.Serve(lis); err != nil {
        log.Fatalf("failed to serve:%v", err)
    }
}
​

client代码

const (
    defaultName = "world"
)var (
    addr = flag.String("addr", "localhost:50051", "the address to connet to")
    name = flag.String("name", defaultName, "Name to greet")
)
​
func main() {
    flag.Parse()
    conn, err := grpc.Dial(*addr, grpc.WithTransportCredentials(insecure.NewCredentials()))
    if err != nil {
        log.Fatalf("did not connect :%v", err)
    }
    defer conn.Close()
    c := helloworld.NewGreeterClient(conn)
    ctx, cancel := context.WithTimeout(context.Background(), time.Second)
    defer cancel()reply, err := c.SayHello(ctx, &helloworld.HelloRequest{Name: *name})
    if err != nil {
        log.Fatalf("could not greet:%v", err)
    }
    log.Printf("Greeting: %s", reply.GetMessage())
}
​

自定义protobu生成代码

Protobuf的protoc编译器是通过插件机制实现对不同语言的支持。比如protoc命令出现--xxx_out格式的参数,那么protoc将首先查询是否有内置的xxx插件,如果没有内置的xxx插件那么将继续查询当前系统中是否存在protoc-gen-xxx命名的可执行程序,最终通过查询到的插件生成代码。对于Go语言的protoc-gen-go插件来说,里面又实现了一层静态插件系统。比如protoc-gen-go内置了一个gRPC插件,用户可以通过--go_out=plugins=grpc参数来生成gRPC相关代码,否则只会针对message生成相关代码。

参考gRPC的源码,插件是有generator.RegisterPlugin函数来的注册的,插件是一个generator.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)
}
  • Name方法返回插件的名字
  • g参数包含proto文件的全部信息
  • Generate用于生成主体代码
  • GenerateImport生成对应的导入包代码

插件代码

  • 新建文件夹custom-plugin
  • 在custom-plugin中新建文件netRpcPlugin.go
package customplugin
​
import (
    "github.com/golang/protobuf/protoc-gen-go/descriptor"
    "github.com/golang/protobuf/protoc-gen-go/generator"
)
​
// 初始化
func init() {
    // 注册插件
    generator.RegisterPlugin(new(netRpcPlugin))
}
​
type netRpcPlugin struct {
    *generator.Generator
}
​
func (p *netRpcPlugin) Name() string {
    return "netrpc"
}
func (p *netRpcPlugin) Init(g *generator.Generator) {
    p.Generator = g
}
​
func (p *netRpcPlugin) GenerateImports(file *generator.FileDescriptor) {
    if len(file.Service) > 0 {
        p.genImportCode(file)
    }
}
func (p *netRpcPlugin) Generate(file *generator.FileDescriptor) {
    for _, svc := range file.Service {
        p.genServiceCode(svc)
    }
}
​
func (p *netRpcPlugin) genImportCode(file *generator.FileDescriptor) {
    p.P("// TODO:import code")
}
​
func (p *netRpcPlugin) genServiceCode(svc *descriptor.ServiceDescriptorProto) {
    p.P("// TODO:service code,Name = " + svc.GetName())
}
​

编译main代码

因为Go语言的包只能静态导入,我们无法向已经安装的protoc-gen-go添加我们新编写的插件。我们将重新克隆protoc-gen-go对应的main函数

package main
​
import (
    // 导包初始化
    _ "blog/custom-plugin"
    "io/ioutil"
    "os""github.com/golang/protobuf/protoc-gen-go/generator"
    "google.golang.org/protobuf/proto"
)func main() {
    g := generator.New()data, err := ioutil.ReadAll(os.Stdin)if err != nil {
        g.Error(err, "reading input")
    }
    if err := proto.Unmarshal(data, g.Request); err != nil {
        g.Error(err, "parsing input proto")
    }
    if len(g.Request.FileToGenerate) == 0 {
        g.Fail("no files to generate")
    }
    g.CommandLineParameters(g.Request.GetParameter())
​
    g.WrapTypes()
    g.SetPackageNames()
    g.GenerateAllFiles()
    data, err = proto.Marshal(g.Response)
    if err != nil {
        g.Error(err, "failer to marshal output proto")
    }
    _, err = os.Stdout.Write(data)
    if err != nil {
        g.Error(err, "failed to write output proto")
    }
}
​
  • 运行bash指令

    $ go build -o  protoc-gen-go-netrpc.exe .
    

    将生成的程序剪贴到$GOPATH$/bin

重新编译proto文件

  • 运行bash指令

    $ protoc --go-netrpc_out=plugins=netrpc:. hello.proto
    

    其中--go-netrpc_out参数告知protoc编译器加载名为protoc-gen-go-netrpc的插件,插件中的plugins=netrpc指示启用内部唯一的名为netrpc的netrpcPlugin插件。在新生成的hello.pb.go文件中将包含增加的注释代码。

参考: