红魔咖啡馆

头发越掉越多,头发越掉越少

0%

【gRPC】Protocol Buffers

用处

Protocol Buffers用于序列化结构化数据,它类似于JSON,但是更小更快,和语言、平台无关

在一个微服务中,若有不同语言想要使用该服务,调用对应API接口,但是由于语言不同,为了支持其他语言,我们需要开发第三方library才能支持

Protobuf解决了这种问题,只需要一个.proto文件定义数据类型与结构或API接口,自带的编译器就可以根据该文件自动生成对应语言的代码

快速上手

编写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 声明版本和命名空间
syntax = "proto3";
package user;

// 导入包
import "google/protobuf/timestamp.proto";
// 声明包名
option go_package = "./user;user"; 

// 声明一个结构体
message User {
  enum Status {
    Unknown = 0;
    Active = 1;
    Inactive = 2;
  }
  int64 id = 1;
  string name = 2;
  string email = 3;
  string password = 4;
  google.protobuf.Timestamp created_at = 5;
  Status UserStatus = 6;
  repeated string phone = 7;
}
  • 首先声明使用的proto版本以及该文件下的命名空间
  • 使用import导入外部包
  • 通过option go_package声明生成的.pb.go文件声明在哪个go包下,导入路径是什么
  • 接下来定义自己的消息(message),除了基本类型,还可以在message中定义message,或定义枚举
  • 若一个字段是repeated,则该字段可以重复任意次数,可以看作动态大小的数组
  • 若一个字段是map,则该字段是一个键值对类型
  • 每个元素的=1等标记标识了该字段在二进制编码中使用的唯一标签,不可以重复使用,从1开始,1-15比其他数字少用一个字节编码,因此可以作为常用或repeated类型的字段,16以上用于不常用的字段
  • 字段编号 19,00019,999 为 Protocol Buffers 实现保留。

编译

使用命令:

1
protoc -I=$SRC_DIR --go_out=$DST_DIR $SRC_DIR/addressbook.proto

其中:

  • -I表示源目录,包括应用程序源代码所在位置和导入protobuf文件的位置
  • --go_out表示生成代码存放的位置,通常和源代码位置相同
  • 第三个参数是自己的protobuf文件路径

Protocol Buffer API

成功生成.pb.go文件后,我们可以像使用任何普通结构体一样使用文件中的类型来构造和读写protobuf数据

生成的结构

假设proto定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
message Person {
  string name = 1;
  int32 id = 2;
  string email = 3;
  repeated PhoneNumber phones = 4;

  message PhoneNumber {
    string number = 1;
    PhoneType type = 2;
  }
  enum PhoneType {
    PHONE_TYPE_UNSPECIFIED = 0;
    PHONE_TYPE_MOBILE = 1;
    PHONE_TYPE_HOME = 2;
    PHONE_TYPE_WORK = 3;
  }
}

message AddressBook {
  repeated Person people = 1;
}

经过编译后,生成的.pb.go文件中会包含如下类型:

  • AddressBook结构体,表示整个通讯录,包含一个People []*Person字段
  • Person结构体,表示一个人,包含他的各种信息字段
  • Person_PhoneNumber结构体,由于是嵌套message,所以生成的名字是父消息_子消息格式,包含Number和Type字段
  • Person_PhoneType类型和他的常量值,对应proto中的枚举

创建一个protobuf对象

1
2
3
4
5
6
7
8
p := pb.Person{
    Id:    1234,
    Name:  "John Doe",
    Email: "jdoe@example.com",
    Phones: []*pb.Person_PhoneNumber{
        {Number: "555-4321", Type: pb.PhoneType_PHONE_TYPE_HOME},
    },
}
  • 对象名前要加包名,这里是将导入的.pb.go文件起了别名

  • Phone是切片,存放的是指针类型,所以要写&pb.Person_PhoneNumber{...}或直接字面量初始化[]*pb.Person_PhoneNumber{}

  • 枚举值会自动添加枚举名前缀

扩展protocal buffer

如果想要更新或扩展proto文件,且让新旧缓冲区前后兼容,需要遵循如下规则:

  • 绝对不可以更改现有字段的标签号
  • 可以删除字段
  • 可以添加新字段,但必须使用新的标签号(被删除字段用过的也不行)
  • 新字段在旧消息中是不存在的,需要处理默认值,默认的默认值为:对于字符串,默认值是空字符串。对于布尔值,默认值是 false。对于数字类型,默认值是零

语法

字段默认值

当解析消息时,如果编码的消息字节中不包含某个特定字段,那么在解析的对象中访问该字段将返回该字段的默认值。默认值是类型特定的:

  • 对于字符串,默认值是空字符串

  • 对于字节,默认值是空字节

  • 对于布尔值,默认值是 false

  • 对于数值类型,默认值是零。

  • 对于消息字段,该字段未设置。其确切值取决于语言

    对于枚举,默认值是第一个定义的枚举值,它必须为 0

对于 repeated 字段,默认值是空的(通常是相应语言中的空列表)

对于 map 字段,默认值是空的(通常是相应语言中的空 map)

枚举

在protobuf文件中使用enum定义枚举

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
enum Corpus {
  CORPUS_UNSPECIFIED = 0;
  CORPUS_UNIVERSAL = 1;
  CORPUS_WEB = 2;
  CORPUS_IMAGES = 3;
  CORPUS_LOCAL = 4;
  CORPUS_NEWS = 5;
  CORPUS_PRODUCTS = 6;
  CORPUS_VIDEO = 7;
}

message SearchRequest {
  string query = 1;
  int32 page_number = 2;
  int32 results_per_page = 3;
  Corpus corpus = 4;
}

枚举值前缀

枚举值添加前缀时,剥离前缀后的其余名称仍应该是合法且符合样式的枚举名,如DEVICE_TIER_1是不对的,应该使用类似DEVICE_TIER_TIER1,因为前缀部分会被视为对枚举值进行作用域限定,而不是作为单个枚举值名称的一部分

枚举默认值

proto3中,枚举定义中定义的第一个值必须为0,且不具有语义含义

CORPUS_UNSPECIFIED = 0;

这样方便我们将0作为数字默认值

序列化与反序列化

Go中可以使用proto库中的函数来序列化和反序列化protocol buffer数据

proto的结构体均默认实现了proto.Message接口,需要传递操作指针

序列化

1
2
3
4
5
6
7
8
9
10
book := &pb.AddressBook{}
// ... 填充 book 数据

out, err := proto.Marshal(book)
if err != nil {
    log.Fatalln("Failed to encode address book:", err)
}
if err := ioutil.WriteFile(fname, out, 0644); err != nil {
    log.Fatalln("Failed to write address book:", err)
}
  • 首先创建指针类型的变量,添加数据
  • proto.Marshal(book)会将这个结构体按照protobuf的二进制格式编码为一串字节
  • 这样out里就算可以保存的二进制数据
  • 这样就可以使用WriteFile()写入文件

反序列化

1
2
3
4
5
6
7
8
in, err := ioutil.ReadFile(fname)
if err != nil {
    log.Fatalln("Error reading file:", err)
}
book := &pb.AddressBook{}
if err := proto.Unmarshal(in, book); err != nil {
    log.Fatalln("Failed to parse address book:", err)
}
  • 首先通过ReadFile()读取文件
  • 准备一个空的结构体指针,用来接收数据
  • 使用proto.Unmarshal()将字节反解成protobuf数据,填充到结构体中

保留字段

对于删除或注释掉的字段,更好的方法是使用reservedreserved之后的字段编号会被标记为保留,若使用这些编号会报错

同理字段名也可以被保留

注意:不能在同一个语句中混合字段名和字段编号

1
2
3
4
message Foo {
  reserved 2, 15, 9 to 11;
  reserved "foo", "bar";
}

映射

定义映射的语法:map<key_type, value_type> map_field = N;

其中key_type可以是任何整数或字符串类型,value_type可以是除另一个map之外的任何类型

注意:

  • map字段不能是repeated
  • 映射值的线路格式顺序和映射迭代顺序是未定义的,所以你不能依赖你的映射项会以特定的顺序排列
  • proto生成文本格式时,映射按键排序,数字键按值排序

服务

若想将消息类型和rpc系统一起调用,可以在.proto文件中定义一个RPC服务接口,例如,如果你想定义一个RPC服务,方法接收SearchRequest并返回一个SearchResponse,可以按如下定义:

1
2
3
service SearchService {
  rpc Search(SearchRequest) returns (SearchResponse);
}

定义时建议添加req和resp参数,并实现message

编译时需要加入一个参数--go-grpc_out指定生成的grpc文件的位置