protobuf在gRPC中的应用
前面已经介绍了protobuf,这里我们先介绍了gRPC,了解gRPC前先了解下什么是RPC
RPC
RPC
也就是remote procedure call
翻译过来就是远程过程调用,RPC是一种概念,grpc实现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文件中将包含增加的注释代码。
参考: