上一篇文章中,我使用net/rpc包实现了一个简单的RPC接口,并尝试了net/rpc自带的Gob编码和JSON编码,学习了Golang的一些基础知识远程过程调用。在这篇文章中,我将结合 net/rpc 和 protobuf 并创建我的 protobuf 插件来帮助我们生成代码,所以让我们开始吧。
本文首发于Medium MPP计划。如果您是 Medium 用户,请在 Medium 上关注我。非常感谢。
我们在工作中肯定使用过gRPC和protobuf,但是它们并没有绑定。 gRPC可以使用JSON编码,protobuf可以使用其他语言实现。
协议缓冲区(Protobuf)是一种免费开源跨平台数据格式,用于序列化结构化数据。它对于开发通过网络相互通信或存储数据的程序很有用。该方法涉及一种描述某些数据结构的接口描述语言和一个根据该描述生成源代码的程序,用于生成或解析表示结构化数据的字节流。
首先我们编写一个原型文件 hello-service.proto ,它定义了一条消息“String”
syntax = "proto3"; package api; option go_package="api"; message String { string value = 1; }
然后使用 protoc 实用程序生成消息字符串的 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,大大降低了插件开发的难度:
The most important thing for each service is the name of the service, and then each service has a set of methods. For the method defined by the service, the most important thing is the name of the method, as well as the name of the input parameter and the output parameter type. Let's first define a ServiceData to describe the meta information of the service:
// ServiceData type ServiceData struct { PackageName string ServiceName string MethodList []Method } // Method type Method struct { MethodName string InputTypeName string OutputTypeName string }
Then comes the main logic, and the code generation logic, and finally the call to tmpl to generate the code.
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) } } }
Finally, we put the compiled binary execution file protoc-gen-go-spprpc in $PATH, and then run protoc to generate the code we want.
protoc --go_out=.. --go-spprpc_out=.. HelloService.proto
Because protoc-gen-go-spprpc has to depend on protoc to run, it's a bit tricky to debug. We can use
fmt.Fprintf(os.Stderr, "Fprintln: %v\n", err)
To print the error log to debug.
That's all there is to this article. We first implemented an RPC call using protobuf and then created a protobuf plugin to help us generate the code. This opens the door for us to learn protobuf + RPC, and is our path to a thorough understanding of gRPC. I hope everyone can master this technology.
以上是RPC 操作 EPU 使用 Protobuf 并创建自定义插件的详细内容。更多信息请关注PHP中文网其他相关文章!