上一篇文章中,我使用net/rpc包实现了一个简单的RPC接口,并尝试了net/rpc自带的Gob编码和JSON编码,学习了Golang的一些基础知识远程过程调用。在这篇文章中,我将结合 net/rpc 和 protobuf 并创建我的 protobuf 插件来帮助我们生成代码,所以让我们开始吧。
本文首发于Medium MPP计划。如果您是 Medium 用户,请在 Medium 上关注我。非常感谢。
我们在工作中肯定使用过gRPC和protobuf,但是它们并没有绑定。 gRPC可以使用JSON编码,protobuf可以使用其他语言实现。
Protocol Buffers(Protobuf)是一种免费开源跨平台数据格式,用于序列化结构化数据。它对于开发通过网络相互通信或存储数据的程序很有用。该方法涉及一种描述某些数据结构的接口描述语言和一个根据该描述生成源代码的程序,用于生成或解析表示结构化数据的字节流。
首先我们编写一个proto文件hello-service.proto,它定义了一条消息“String”
syntax = "proto3"; package api; option go_package="api"; message String { string value = 1; }
然后使用protoc实用程序生成消息String的Go代码
protoc --go_out=. hello-service.proto
然后我们修改Hello函数的参数以使用protobuf文件生成的字符串。
type HelloServiceInterface = interface { Hello(request api.String, reply *api.String) error }
使用起来和以前没有什么不同,甚至不如直接使用string方便。那么我们为什么要使用protobuf呢?正如我前面所说,使用Protobuf定义与语言无关的RPC服务接口和消息,然后使用protoc工具生成不同语言的代码,才是它真正的价值所在。例如使用官方插件protoc-gen-go生成gRPC代码。
protoc --go_out=plugins=grpc. hello-service.proto
要从 protobuf 文件生成代码,我们必须安装 protoc ,但是 protoc 不知道我们的目标语言是什么,所以我们需要插件来帮助我们生成代码。 protoc的插件系统如何工作?以上面的grpc为例。
这里有一个--go_out参数。由于我们调用的插件是protoc-gen-go,因此参数称为go_out;如果名称为 XXX,则该参数将被称为 XXX_out.
protoc运行时,首先会解析protobuf文件,生成一组Protocol Buffers编码的描述性数据。它会首先判断protoc中是否包含go插件,然后会尝试在$PATH中寻找protoc-gen-go,如果找不到就会报错,然后将运行 protoc-gen-go。 protoc-gen-go 命令并通过 stdin 将描述数据发送到插件命令。插件生成文件内容后,会将Protocol Buffers编码的数据输入到stdout,告诉protoc生成特定的文件。
plugins=grpc 是 protoc-gen-go 附带的一个插件,以便调用它。如果你不使用它,它只会在Go中生成一条消息,但是你可以使用这个插件来生成grpc相关的代码。
如果我们在protobuf中添加Hello接口时序,是否可以自定义一个protoc插件直接生成代码?
syntax = "proto3"; package api; option go_package="./api"; service HelloService { rpc Hello (String) returns (String) {} } message String { string value = 1; }
对于本文,我的目标是创建一个插件,然后用于生成 RPC 服务器端和客户端代码,如下所示。
// HelloService_rpc.pb.go type HelloServiceInterface interface { Hello(String, *String) error } func RegisterHelloService( srv *rpc.Server, x HelloServiceInterface, ) error { if err := srv.RegisterName("HelloService", x); err != nil { return err } return nil } type HelloServiceClient struct { *rpc.Client } var _ HelloServiceInterface = (*HelloServiceClient)(nil) func DialHelloService(network, address string) ( *HelloServiceClient, error, ) { c, err := rpc.Dial(network, address) if err != nil { return nil, err } return &HelloServiceClient{Client: c}, nil } func (p *HelloServiceClient) Hello( in String, out *String, ) error { return p.Client.Call("HelloService.Hello", in, out) }
这会将我们的业务代码更改为如下所示
// service func main() { listener, err := net.Listen("tcp", ":1234") if err != nil { log.Fatal("ListenTCP error:", err) } _ = api.RegisterHelloService(rpc.DefaultServer, new(HelloService)) for { conn, err := listener.Accept() if err != nil { log.Fatal("Accept error:", err) } go rpc.ServeConn(conn) } } type HelloService struct{} func (p *HelloService) Hello(request api.String, reply *api.String) error { log.Println("HelloService.proto Hello") *reply = api.String{Value: "Hello:" request.Value} return nil } // client.go func main() { client, err := api.DialHelloService("tcp", "localhost:1234") if err != nil { log.Fatal("net.Dial:", err) } reply := &api.String{} err = client.Hello(api.String{Value: "Hello"}, reply) if err != nil { log.Fatal(err) } log.Println(reply) }
根据生成的代码,我们的工作量已经小很多了,出错的机会也已经很小了。一个好的开始。
根据上面的api代码,我们可以拉出一个模板文件:
const tmplService = ` import ( "net/rpc") type {{.ServiceName}}Interface interface { func Register{{.ServiceName}}( if err := srv.RegisterName("{{.ServiceName}}", x); err != nil { return err } return nil} *rpc.Client} func Dial{{.ServiceName}}(network, address string) ( {{range $_, $m := .MethodList}} return p.Client.Call("{{$root.ServiceName}}.{{$m.MethodName}}", in, out)} `
整个模板很清晰,里面有一些占位符,比如MethodName、ServiceName等,我们稍后会介绍。
Google发布了Go语言API 1,引入了新的包google.golang.org/protobuf/compile R/protogen,大大降低了插件开发的难度:
每个服务最重要的是服务的名称,然后每个服务都有一套方法。对于服务定义的方法来说,最重要的是方法的名称,以及输入参数的名称和输出参数类型。我们先定义一个ServiceData来描述服务的元信息:
// ServiceData type ServiceData struct { PackageName string ServiceName string MethodList []Method } // Method type Method struct { MethodName string InputTypeName string OutputTypeName string }
然后是主逻辑,以及代码生成逻辑,最后是调用tmpl生成代码。
func main() { protogen.Options{}.Run(func(gen *protogen.Plugin) error { for _, file := range gen.Files { if !file.Generate { continue } generateFile(gen, file) } return nil }) } // generateFile function definition func generateFile(gen *protogen.Plugin, file *protogen.File) { filename := file.GeneratedFilenamePrefix "_rpc.pb.go" g := gen.NewGeneratedFile(filename, file.GoImportPath) tmpl, err := template.New("service").Parse(tmplService) if err != nil { log.Fatalf("Error parsing template: %v", err) } packageName := string(file.GoPackageName) // Iterate over each service to generate code for _, service := range file.Services { serviceData := ServiceData{ ServiceName: service.GoName, PackageName: packageName, } for _, method := range service.Methods { inputType := method.Input.GoIdent.GoName outputType := method.Output.GoIdent.GoName serviceData.MethodList = append(serviceData.MethodList, Method{ MethodName: method.GoName, InputTypeName: inputType, OutputTypeName: outputType, }) } // Perform template rendering err = tmpl.Execute(g, serviceData) if err != nil { log.Fatalf("Error executing template: %v", err) } } }
最后,我们将编译好的二进制执行文件 protoc-gen-go-spprpc 放入 $PATH 中,然后运行 protoc 生成我们想要的代码。
protoc --go_out=.. --go-spprpc_out=.. HelloService.proto
因为protoc-gen-go-spprpc必须依赖protoc才能运行,所以调试起来有点棘手。我们可以使用
fmt.Fprintf(os.Stderr, "Fprintln: %v\n", err)
打印错误日志进行调试。
这就是本文的全部内容。我们首先使用protobuf实现了一个RPC调用,然后创建了一个protobuf插件来帮助我们生成代码。这为我们学习protobuf RPC打开了大门,也是我们深入了解gRPC的途径。希望大家都能掌握这项技术。
免责声明: 提供的所有资源部分来自互联网,如果有侵犯您的版权或其他权益,请说明详细缘由并提供版权或权益证明然后发到邮箱:[email protected] 我们会第一时间内为您处理。
Copyright© 2022 湘ICP备2022001581号-3