目录

go rpc系列2-protobuf

protobuf 语法

Protobuf 是 Protocol Buffers 的简称,是一种与语言、平台无关,可扩展的序列化结构化数据的数据描述语言,Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具。

hello.proto 文件

syntax = "proto3";

package main;

message String {
    string value = 1;
}
syntax = "proto3";

package greeter;

option go_package="proto1/greeter";

service Greeter {
    rpc SayHello (HelloRequest) returns (HelloReply) {}
}
message HelloRequest {
    string name = 1;
}
message HelloReply {
    string message = 1;
}
  • 第一行声明使用 proto3 语法。否则,默认使用 proto2 语法,目前主流推荐使用 v3 版本。此声明必须是文件的非空、非注释的第一行。
  • package 指令指明当前是 main 包,用户也可以针对不同的语言定制对应的包路径和名称。
  • message 关键字定义一个 String 类型消息体,在最终生成的Go语言代码中对应一个 String 结构体。每一个消息体的字段包含三个属性:类型、字段名称、字段编号。在消息体的定义上,除类型以外均不可重复。此处 String 类型中只有一个字符串类型的 value 成员,该成员编码时用1编号代替名字。
  • Protobuf 中最基本的数据单元是 message,类似 Go 语言中的结构体。在 message 中可以嵌套 message 或其它的基础数据类型的成员。

package

proto文件(非protoc)有两个易混参数,即package和xx_package,xx指的是你的编译语言,比如你要编程成Go语言,对应的就是go_package。

package

package参数针对的是protobuf,是proto文件的命名空间,它的作用是为了避免我们定义的接口,或者message出现冲突。 举一个小栗子,假设我有A.proto和B.proto两份文件,如下 A.proto

message UserInfo {
    uint32 uid = 1;
    string name = 2;
}

B.proto

message UserInfo {
    uint32 uid = 1;
    string name = 2;
    uint32 age = 3;
    string work = 4;
}

如上,两份文件同时有一个UserInfo的message,这时候如果我需要在A文件引用B文件,如果没有指定package,就无法区分是要调A的UserInfo还是调B的。

xx_package

这里以go_package进行举例说明,该参数主要声明Go代码的存放位置,也可以说它解决的是包名问题(因为proto文件编译后会生成一份.pb.go文件,既然是go文件,就有包名问题) .pb.go常规的存放路径一般是放在同名proto文件下,但也有些人不想这么做,比如他想把所有.pb.go文件都存放在一个特定文件夹下,比如上述的 pb_go,那么他有两种办法: 第一种: 修改 –go_out,go_package 保持不变 $ protoc –proto_path=. –go_out=./proto1/pb_go proto1/greeter/greeter.proto

这样生成的pb文件在 pb_go/proto1/greeter 目录下,文件目录有点冗余,不过pb文件的包名仍然是 greeter

第二种: 修改 go_package, go_out 保持不变 option go_package=“proto1/pb_go”;

$ protoc –proto_path=. –go_out=. proto1/greeter/greeter_v2.proto

这样生成的pb文件在 pb_go 目录下,pb文件的包名为 pb_go

关于标识号

消息体中字段定义了唯一的数字值。这些数字是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。注:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1,15]之内的标识号。

最小的标识号可以从1开始,最大到2^29 - 1, or 536,870,911。不可以使用其中的[19000-19999]的标识号, Protobuf 协议实现中对这些进行了预留。如果非要在 .proto 文件中使用这些预留标识号,编译时就会报警。类似地,你不能使用之前保留的任何标识符。

添加注释

.proto 文件添加注释,可以使用C/C++风格的 // 和 /* … */ 语法格式

保留字段

如果从前面定义的消息中删除了 和 字段,应保留其字段编号,使用关键字 reserved:

syntax "proto3";

message Stock {
    reserved 3, 4;
    // ...
}

还可以将 reserved 关键字用作将来可能添加的字段的占位符。可以使用 to 关键字将连续字段号占位。

syntax "proto3";

message Info {
    reserved 2, 9 to 11, 15;
    // ...
}

protoc命令

安装

$ brew install protoc

查看版本

$ protoc –version libprotoc 3.7.1

参数

protoc --proto_path=. --go_out=. proto1/greeter/greeter.proto

上面的指令可以拆解为三部分,分别对应protoc的三个重要参数,我们首先来看看protoc提供了哪些参数:

$ protoc --help
Usage: protoc [OPTION] PROTO_FILES

  -IPATH, --proto_path=PATH   指定搜索路径
  --plugin=EXECUTABLE:
  
  ....
 
  --cpp_out=OUT_DIR           Generate C++ header and source.
  --csharp_out=OUT_DIR        Generate C# source file.
  --java_out=OUT_DIR          Generate Java source file.
  --js_out=OUT_DIR            Generate JavaScript source.
  --objc_out=OUT_DIR          Generate Objective C header and source.
  --php_out=OUT_DIR           Generate PHP source file.
  --python_out=OUT_DIR        Generate Python source file.
  --ruby_out=OUT_DIR          Generate Ruby source file
  
   @<filename>                proto文件的具体位置
  • 搜索路径参数 第一个比较重要的参数就是搜索路径参数,即上述展示的-IPATH, –proto_path=PATH。它表示的是我们要在哪个路径下搜索.proto文件,这个参数既可以用-I指定,也可以使用–proto_path=指定。 如果不指定该参数,则默认在当前路径下进行搜索;另外,该参数也可以指定多次,这也意味着我们可以指定多个路径进行搜索。

  • 语言插件参数 语言参数即上述的–cpp_out=,–python_out=等,protoc支持的语言长达13种,且都是比较常见的。像上面出现的语言参数,说明protoc本身已经内置该语言对应的编译插件,我们无需安装。 而如果上面没出现的,比如–go_out=,就得自己单独安装语言插件,比如–go_out=对应的是protoc-gen-go

–go_out详细解读

想必大家在使用的时候,应该遇到过这些写法:–go_out=paths=import:.、–go_out=paths=source_relative:.,或者–go_out=plugins=grpc:.。 这样写表达的是啥意思呢? 所以我们需要知道,–go_out参数是用来指定 protoc-gen-go 插件的工作方式和Go代码的生成位置,而上面的写法正是表明该插件的工作方式。

–go_out主要的两个参数为plugins 和 paths,分别表示生成Go代码所使用的插件,以及生成的Go代码的位置。–go_out的写法是,参数之间用逗号隔开,最后加上冒号来指定代码的生成位置,比如–go_out=plugins=grpc,paths=import:.

plugins参数有不带grpc和带grpc两种(应该还有其它的,目前知道的有这两种),两者的区别如下,带grpc的会多一些跟gRPC相关的代码,实现gRPC通信。

paths参数有两个选项,分别是 import 和 source_relative,默认为 import,表示按照生成的Go代码的包的全路径去创建目录层级,source_relative 表示按照 proto源文件的目录层级去创建Go代码的目录层级,如果目录已存在则不用创建。

生成相应的Go代码

Protobuf 核心的工具集是 C++ 语言开发的,官方的 protoc 编译器中并不支持Go语言。要想基于上面 的 hello.proto 文件生成相应的Go代码,需要安装相应的插件。

安装官方的 protoc 工具,可以从 https://github.com/google/protobuf/releases 下载。 安装针对Go语言的代码生成插件,通过 go get github.com/golang/protobuf/protoc-gen-go 命令安装。 通过以下命令生成相应的Go代码:

$ protoc –go_out=. hello.proto

go_out 参数告知 protoc 编译器去加载对应的 protoc-gen-go 工具,生成的代码放到当前目录。最后是一系列要处理的protobuf文件的列表。

plugins=plugin1+plugin2:指定要加载的子插件列表,我们定义的 proto 文件是涉及了 RPC 服务的,而默认是不会生成 RPC 代码的,因此需要在 go_out 中给出 plugins 参数传递给 protoc-gen-go,告诉编译器,请支持 RPC(这里指定了内置的 grpc 插件)。 todo

基本数据类型

protobuf 所生成出来的数据类型并非与原始的类型完全一致,下面是一些常见的类型映射:

Protobuf 和 RPC组合

基于 String 类型,重新实现 HelloService 服务

package main

import (
 "log"
 "net"
 "net/rpc"
 "rpc/protoc"
)

// HelloService is rpc server obj
type HelloService struct{}

//Hello方法的输入参数和输出的参数均改用 Protobuf 定义的 String 类型表示。
//因为新的输入参数为结构体类型,因此改用指针类型作为输入参数,函数的内部代码同时也做了相应的调整。
func (p *HelloService) Hello(request *protoc.String, reply *protoc.String) error {
 reply.Value = "hello:" + request.GetValue()
 return nil
}

func main() {
 rpc.RegisterName("HelloService", new(HelloService))

 listener, err := net.Listen("tcp", ":1234")
 if err != nil {
  log.Fatal("ListenTCP error:", err)
 }

 conn, err := listener.Accept()
 if err != nil {
  log.Fatal("Accept error", err)
 }

 rpc.ServeConn(conn)
}

客户端请求HelloService服务的代码 client.go:

package main

import (
 "fmt"
 "log"
 "net/rpc"
 "rpc/protoc"
)

func main() {
 client, err := rpc.Dial("tcp", "localhost:1234")
 if err != nil {
  log.Fatal("dialing err:", err)
 }

 var reply = &protoc.String{}
 var param = &protoc.String{
  Value: "hello wekenw",
 }

 err = client.Call("HelloService.Hello", &param, &reply)
 if err != nil {
  log.Fatal(err)
 }
 fmt.Println(reply)
 }