avatar

Protobuf是Protocol Buffers的简称,它是Google公司开发的一种数据描述语言,并于2008年对外开源。Protobuf刚开源时的定位类似于XML、JSON等数据描述语言,通过附带工具生成代码并实现将结构化数据序列化的功能。但是我们更关注的是Protobuf作为接口规范的描述语言,可以作为设计安全的跨语言PRC接口的基础工具

官网

为什么选择Protobuf

一般而言我们需要一种编解码工具会参考:

  • 编解码效率
  • 高压缩比
  • 多语言支持

其中压缩与效率 最被关注的点:

avatar

使用流程

首先需要定义我们的数据,通过编译器,来生成不同语言的代码
avatar

之前我们的RPC要么使用的Gob, 要么使用的json, 接下来我们将使用probuf

首先创建hello.proto文件,其中包装HelloService服务中用到的字符串类型

1
2
3
4
5
6
7
8
syntax = "proto3";

package hello;
option go_package="pbrpc/pb";

message String {
string value = 1;
}
  • syntax: 表示采用proto3的语法。第三版的Protobuf对语言进行了提炼简化,所有成员均采用类似Go语言中的零值初始化(不再支持自定义默认值),因此消息成员也不再需要支持required特性。
  • package:指明当前是main包(这样可以和Go的包名保持一致,简化例子代码),当然用户也可以针对不同的语言定制对应的包路径和名称。
  • option:protobuf的一些选项参数, 这里指定的是要生成的Go语言package路径, 其他语言参数各不相同
  • message: 关键字定义一个新的String类型,在最终生成的Go语言代码中对应一个String结构体。String类型中只有一个字符串类型的value成员,该成员编码时用1编号代替名字

关于数据编码:

在XML或JSON等数据描述语言中,一般通过成员的名字来绑定对应的数据。但是Protobuf编码却是通过成员的唯一编号来绑定对应的数据,因此Protobuf编码后数据的体积会比较小,但是也非常不便于人类查阅。我们目前并不关注Protobuf的编码技术,最终生成的Go结构体可以自由采用JSON或gob等编码格式,因此大家可以暂时忽略Protobuf的成员编码部分,
但是我们如何把这个定义文件(IDL: 接口描述语言), 编译成不同语言的数据结构喃? 着就需要我们安装protobuf的编译器

安装编译器

protobuf的编译器叫: protoc(protobuf compiler), 我们需要到这里下载编译器: Github Protobuf

安装Go语言插件

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

1
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest

编译proto文件

1
protoc -I=./pb_rpc/codec/pb --go_out=./pb_rpc/codec/pb --go_opt=module="test-grpc/pb_rpc/codec/pb" ./pb_rpc/codec/pb/*.proto
  • -I:-IPATH, --proto_path=PATH, 指定proto文件搜索的路径, 如果有多个路径 可以多次使用-I 来指定, 如果不指定默认为当前目录
  • –go_out: --go指插件的名称, 我们安装的插件为: protoc-gen-go, 而protoc-gen是插件命名规范, go是插件名称, 因此这里是–go, 而–go_out 表示的是 go插件的 out参数, 这里指编译产物的存放目录
  • –go_opt: protoc-gen-go插件opt参数, 这里的module指定了go module, 生成的go pkg 会去除掉module路径,生成对应pkg
  • pb/*.proto: 我们proto文件路径

这样我们就在当前目录下生成了Go语言对应的pkg, 我们的message String 被生成为了一个Go Struct

序列化与反序列化

基于上面生成的Go 数据结构, 我们就可以来进行 数据的交互了(序列化与反序列化)

我们使用google.golang.org/protobuf/proto工具提供的API来进行序列化与反序列化:

  • Marshal: 序列化
  • Unmarshal: 反序列化

下来来模拟一个 客户端 —> 服务端 基于protobuf的数据交互过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
package main

import (
"fmt"
"log"

"google.golang.org/protobuf/proto"

"testpb/pb"
)

func main() {
clientObj := &pb.String{Value: "hello proto3"}

// 序列化
out, err := proto.Marshal(clientObj)
if err != nil {
log.Fatalln("Failed to encode obj:", err)
}

// 二进制编码
fmt.Println("encode bytes: ", out)

// 反序列化
serverObj := &pb.String{}
err = proto.Unmarshal(out, serverObj)
if err != nil {
log.Fatalln("Failed to decode obj:", err)
}
fmt.Println("decode obj: ", serverObj)
}

// encode bytes: [10 12 104 101 108 108 111 32 112 114 111 116 111 51]
// decode obj: value:"hello proto3"

proto3语法

定义消息类型

1
2
3
4
5
6
7
8
9
10
syntax = "proto3";

/* SearchRequest represents a search query, with pagination options to
* indicate which results to include in the response. */

message SearchRequest {
string query = 1;
int32 page_number = 2; // Which page number do we want?
int32 result_per_page = 3;
}

第一行是protobuf的版本, 我们主要讲下 message 的定义语法:

1
2
3
4
5
6
<comment>

message <message_name> {
<filed_rule> <filed_type> <filed_name> = <field_number>
类型 名称 编号
}
  • comment: 注射 /* */或者 //
  • message_name: 同一个pkg内,必须唯一
  • filed_rule: 可以没有, 常用的有repeated, oneof
  • filed_type: 数据类型, protobuf定义的数据类型, 生产代码的会映射成对应语言的数据类型
  • filed_name: 字段名称, 同一个message 内必须唯一
  • field_number: 字段的编号, 序列化成二进制数据时的字段编号, 同一个message 内必须唯一, 1 ~ 15 使用1个Byte表示, 16 ~ 2047 使用2个Byte表示

如果你想保留一个编号,以备后来使用可以使用 reserved 关键字声明

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

Value(Filed) Types

protobuf 定义了很多Value Types, 他和其他语言的映射关系如下:

avatar

上面就是所有的protobuf基础类型, 光有这些基础类型是不够的, 下面是protobuf为我们提供的一些复合类型

枚举类型

使用enum来声明枚举类型:

1
2
3
4
5
6
7
8
9
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}

枚举声明语法:

1
2
3
enum <enum_name> {
<element_name> = <element_number>
}
  • enum_name: 枚举名称
  • element_name: pkg内全局唯一, 很重要
  • element_name: 必须从0开始, 0表示类型的默认值, 32-bit integer

别名

如果你的确有2个同名的枚举需求: 比如 TaskStatus 和 PipelineStatus 都需要Running,就可以添加一个: option allow_alias = true;

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
message MyMessage1 {
enum EnumAllowingAlias {
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
message MyMessage2 {
enum EnumNotAllowingAlias {
UNKNOWN = 0;
STARTED = 1;
// RUNNING = 1; // Uncommenting this line will cause a compile error inside Google and a warning message outside.
}
}

预留值

同理枚举也支持预留值

1
2
3
4
enum Foo {
reserved 2, 15, 9 to 11, 40 to max;
reserved "FOO", "BAR";
}

数组类型

如果我们想声明: []string, []Item 这在数组类型怎么办? filed_rule: repeated 可以胜任

1
2
3
4
5
6
7
8
message SearchResponse {
repeated Result results = 1;
}

// 会编译为:
// type SearchResponse SearchResponse {
// results []*Result
// }

Map

如果我们想声明一个map, 可以如下进行:

1
2
map<string, Project> projects = 3;
// projects map[string, Project]

protobuf 声明map的语法:

1
map<key_type, value_type> map_field = N;

Oneof

很像范型 比如 test_oneof 字段的类型 必须是 string name 和 SubMessage sub_message 其中之一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message Sub1 {
string name = 1;
}

message Sub2 {
string name = 1;
}

message SampleMessage {
oneof test_oneof {
Sub1 sub1 = 1;
Sub2 sub2 = 2;
}
}

编译过后结构体

1
2
3
4
5
6
7
8
9
10
type SampleMessage struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

// Types that are assignable to TestOneof:
// *SampleMessage_Sub1
// *SampleMessage_Sub2
TestOneof isSampleMessage_TestOneof `protobuf_oneof:"test_oneof"`
}

Any

当我们无法明确定义数据类型的时候, 可以使用Any表示:

1
2
3
4
5
6
7
// 这里是应用其他的proto文件, 后面会讲 ipmort用法
import "google/protobuf/any.proto";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}

any本质上就是一个bytes数据结构

1
2
3
4
5
6
7
8
type ErrorStatus struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

Message string `protobuf:"bytes,1,opt,name=message,proto3" json:"message,omitempty"`
Details []*anypb.Any `protobuf:"bytes,2,rep,name=details,proto3" json:"details,omitempty"`
}

下面是 any的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
// `Any` contains an arbitrary serialized protocol buffer message along with a
// URL that describes the type of the serialized message.
//
// Protobuf library provides support to pack/unpack Any values in the form
// of utility functions or additional generated methods of the Any type.
// Example 4: Pack and unpack a message in Go
//
// foo := &pb.Foo{...}
// any, err := anypb.New(foo)
// if err != nil {
// ...
// }
// ...
// foo := &pb.Foo{}
// if err := any.UnmarshalTo(foo); err != nil {
// ...
// }
//
// The pack methods provided by protobuf library will by default use
// 'type.googleapis.com/full.type.name' as the type URL and the unpack
// methods only use the fully qualified type name after the last '/'
// in the type URL, for example "foo.bar.com/x/y.z" will yield type
// name "y.z".
//
//
// JSON
// ====
// The JSON representation of an `Any` value uses the regular
// representation of the deserialized, embedded message, with an
// additional field `@type` which contains the type URL. Example:
//
// package google.profile;
// message Person {
// string first_name = 1;
// string last_name = 2;
// }
//
// {
// "@type": "type.googleapis.com/google.profile.Person",
// "firstName": <string>,
// "lastName": <string>
// }
//
// If the embedded message type is well-known and has a custom JSON
// representation, that representation will be embedded adding a field
// `value` which holds the custom JSON in addition to the `@type`
// field. Example (for message [google.protobuf.Duration][]):
//
// {
// "@type": "type.googleapis.com/google.protobuf.Duration",
// "value": "1.212s"
// }
//
type Any struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields

...
// Note: this functionality is not currently available in the official
// protobuf release, and it is not used for type URLs beginning with
// type.googleapis.com.
//
// Schemes other than `http`, `https` (or the empty scheme) might be
// used with implementation specific semantics.
//
TypeUrl string `protobuf:"bytes,1,opt,name=type_url,json=typeUrl,proto3" json:"type_url,omitempty"`
// Must be a valid serialized protocol buffer of the above specified type.
Value []byte `protobuf:"bytes,2,opt,name=value,proto3" json:"value,omitempty"`
}

类型嵌套

我们可以再message里面嵌套message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
message Outer {                  // Level 0
message MiddleAA { // Level 1
message Inner { // Level 2
int64 ival = 1;
bool booly = 2;
}
}
message MiddleBB { // Level 1
message Inner { // Level 2
int32 ival = 1;
bool booly = 2;
}
}
}

与Go结构体嵌套一样, 但是不允许 匿名嵌套, 必须指定字段名称

引用包

1
2
// 这里是应用其他的proto文件,  ipmort用法
import "google/protobuf/any.proto";

上面这在情况就是读取的标准库, 我们在安装protoc的时候, 已经把改lib 挪到usr/local/include下面了,所以可以找到

如果我们proto文件并没有在/usr/local/include目录下, 我们如何导入,比如:

1
import "myproject/other_protos.proto";

通过-I 可以添加搜索的路径, 这样就编译器就可以找到我们引入的包了

引入后通过包的名称.变量的方式使用

比如我们要应用该结构中的ErrorStatus

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";

// 这里是应用其他的proto文件, 后面会讲 ipmort用法
import "google/protobuf/any.proto";

package hello;
option go_package="gitee.com/infraboard/go-course/day21/pb";

message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}

引入ErrorStatus

1
2
3
4
5
6
7
8
9
10
11
12
syntax = "proto3";

// 由于这个文件的pkg 也叫hello, 因此我们可以不用添加 pkg前缀
// 如果不是同一个pkg 就需要添加 pkg名称前缀, 比如hello.ErrorStatus
import "pb/any.proto";

package hello;
option go_package="gitee.com/infraboard/go-course/day21/pb";

message ErrorStatusExt {
ErrorStatus error_status = 1;
}