02-protocol buffer

1、ProtoBuf 介绍

gRPC 使用了 Protocol buffers。

Protocol buffers,通常称为 Protobuf,是谷歌开发的一款无关平台,无关语言,可扩展,轻量级高效的序列化结构的数据格式,用于将自定义数据结构序列化成字节流,和将字节流反序列化为数据结构。所以很适合做数据存储和为不同语言,不同应用之间互相通信的数据交换格式,只要实现相同的协议格式,即后缀为 proto 文件被编译成不同的语言版本,加入各自的项目中,这样不同的语言可以解析其它语言通过 Protobuf 序列化的数据。目前官方提供 c++,java,go 等语言支持。

我们可以把他当成一个 代码生成工具 以及 序列化工具。这个工具可以把我们定义的方法,转换成特定语言的代码。比如 Golang,你定义的一种类型的参数,他会帮你转换成 Golang 中的结构体,你定义的方法,他会帮你转换成 func 函数。此外,在发送请求和接受响应的时候,这个工具还会完成对应的编码和解码工作,将你即将发送的数据编码成 gRPC 能够传输的形式,又或者将即将接受到的数据解码成编程语言所能理解的数据格式。

  • 序列化: 将数据结构或对象转换成二进制串的过程
  • 反序列化: 将在序列化过程中产生的二进制串转换成数据结构或对象的过程

支持的语言列表,以及支持每种语言对应的安装说明:

Language Source
C++ (include C++ runtime and protoc) src
Java java
Python python
Objective-C objectivec
C# csharp
Ruby ruby
Go protocolbuffers/protobuf-go
PHP php
Dart dart-lang/protobuf
JavaScript protocolbuffers/protobuf-javascript

Protobuf 用来做什么

protobuf 是跨平台的数据交互格式,通过字节流的方式在不同终端或服务器之间进行数据传输。

目前主要有以下数据交换格式:

  • JSON:一般用于 WEB 项目中,因为浏览器对 JSON 格式的数据支持非常好,大部分编程语言有很多内建函数支持,而且 JSON 几乎支持所有编程语言。用于 RESTful 通信协议。
  • xml:XML 在 WebService 中的应用比较多,相比于 JSON,它的数据更加冗余,因为需要成对的闭合标签,而 JSON 使用了键值对的方式,不仅压缩了一定的数据空间,同时也有更好的可读性。
  • protobuf:谷歌公司新开发的一种数据格式,适合高性能,对响应速度有要求的数据传输场景。因为 Protobuf 是二进制数据格式,需要编码和解码。数据本身不具有可读性,因此只能反序列化得到可读数据。用于 gRPC 通信协议。

Protobuf 的优点和缺点

优点:

  1. Protobuf 序列化和反序列化速度快,序列化后的体积比 JSON/xml 更小,传输更快。使用相对也简单,因为 Proto 编译器能自己序列化和反序列化。
  2. 可以定义自己的数据结构,然后使用代码生成器去生成的代码来读写这个数据结构,甚至可以在不用重新部署的情况下来更新这个数据结构,只需要使用 Protobuf 对数据结构进行一次描述,就可以利用不同的语言或者从不同的数据流对你的结构化数据轻松的读写。
  3. 向后兼容性好,不需要破坏旧的数据格式,依靠老的数据格式的程序就可以对数据结构更新。
  4. 语义比 xml 更加清晰,无需类似 xml 解析器的东西(因为 Protobuf 编译器会将 .proto 文件编译成对应的数据访问用以对Protobuf 数据进行序列化和反序列化操作)。
  5. 跨平台,跨语言,可扩展性好。
  6. 维护成本比较低,多个平台只需要维护一套 .proto 对象协议文件。
  7. 加密性好。

缺点:

  1. Protobuf 功能简单,无法用来表示复杂的概念。
  2. 相比 xml,xml 具有某种程度的自解释性,因为最终是转成二进制流,不像 xml 和 JSON 能够直接查看明文。

gRPC VS RESTful

平时我们对接接口大都使用 RESTful 协议比较多,gRPC 能为我们解决什么样的问题又能带来什么样的体验,知乎的一篇文章分析很详细引用如下:

gRPC 和 RESTful 之间的对比,历来是学习 gRPC 的必修课,我会从文档规范、消息编码、传输协议、传输性能、传输形式、浏览器的支持度以及数据的可读性、安全性等方面进行比较。

  1. 文档规范

    文档规范这种东西有点见仁见智,在我看来,gRPC 使用 proto 文件编写接口(API),文档规范比 RESTful 更好,因为 proto 文件的语法和形式是定死的,所以更为严谨、风格统一清晰;而 RESTful 由于可以使用多种工具进行编写(只要人看得懂就行),每家公司、每个人的攥写风格又各有差异,难免让人觉得比较混乱

    另外,RESTful 文档的过时相信很多人深有体会,因为维护一份不会过时的文档需要很大的人力和精力,而公司往往都是业务为先;而 gRPC 文档即代码,接口的更改也会体现到代码中,这也是我比较喜欢 gRPC 的一个原因,因为不用花很多精力去维护文档

  2. 消息编码

    消息编码这块,gRPC 使用 protobuf 进行消息编码,而 RESTful 一般使用 JSON 进行编码

  3. 传输协议

    传输协议这块,gRPC 使用 HTTP/2 作为底层传输协议,据说也可替换为其他协议,但目前还未考证;而 RESTful 则使用 HTTP

    RPC 和 RESTful 都是微服务间通信较为常用的方案之一,其实 RPC 和 RESTful 并不完全是同一个层次的概念,它们之间还是有所区别的。

    • RPC 是远程过程调用,其调用协议通常包括序列化协议和传输协议。序列化协议有基于纯文本的 XML 和 JSON、二进制编码的Protobuf 和 Hessian。传输协议是指其底层网络传输所使用的协议,比如 TCP、HTTP。
    • 可以看出 HTTP 是 RPC 的传输协议的一个可选方案,比如说 gRPC 的网络传输协议就是 HTTP。HTTP 既可以和 RPC 一样作为服务间通信的解决方案,也可以作为 RPC 中通信层的传输协议(此时与之对比的是 TCP 协议)。
  4. 传输性能

    由于 gRPC 使用 protobuf 进行消息编码(即序列化),而经 protobuf 序列化后的消息体积很小(传输内容少,传输相对就快);再加上 HTTP/2 协议的加持(HTTP1.1的进一步优化),使得 gRP C的传输性能要优于 RESTful。

  5. 传输形式

    传输形式这块,gRPC 最大的优势就是支持流式传输,传输形式具体可以分为四种(unary、client stream、server stream、bidirectional stream),这个后面我们会讲到;而 RESTful 是不支持流式传输的。

  6. 浏览器的支持度

    不知道是不是 gRPC 发展较晚的原因,目前浏览器对 gRPC 的支持度并不是很好,而对 RESTful 的支持可谓是密不可分,这也是gRPC 的一个劣势,如果后续浏览器对 gRPC 的支持度越来越高,不知道 gRPC 有没有干饭 RESTful 的可能呢?

  7. 消息的可读性和安全性

    由于 gRPC 序列化的数据是二进制,且如果你不知道定义的 Request 和 Response 是什么,你几乎是没办法解密的,所以 gRPC 的安全性也非常高,但随着带来的就是可读性的降低,调试会比较麻烦;而 RESTful 则相反(现在有 HTTPS,安全性其实也很高)

  8. 代码的编写

    由于 gRPC 调用的函数,以及字段名,都是使用 stub 文件的,所以从某种角度看,代码更不容易出错,联调成本也会比较低,不会出现低级错误,比如字段名写错、写漏。

2、安装 ProtoBuf

安装 ProtoBuf

  1. 下载 protocol buffers: https://github.com/protocolbuffers/protobuf/releases

    • RC(Release Candidate)版本指的是“发布候选版”。RC版本是软件开发过程中的一个阶段,通常在软件即将正式发布之前发布。这个版本的软件已经包含了所有预期的功能,并且已经接近最终版本。在RC版本中,通常不会引入新的功能,而是主要集中在对现有功能的错误和缺陷进行修复。如果RC版本没有发现重大问题,那么它很快就会升级为正式版本。
  2. 根据自己的系统选择合适的安装包进行下载,解压,并将 bin 目录配置到环境变量中即可

  3. 最后在 cmd 或 bash 中输入 protoc --version 命令查看是否安装并配置成功

go

安装 gRPC 核心库

1
go get google.golang.org/grpc

上面安装的是protocol编译器。而上文中我们提到了 ProtoBuf 可以生成各种不同语言的代码。因此,除了这个编译器,我们还需要配合各个语言的代码生成工具。

对于 Golang 来说,这个工具是protoc-gen-go

这里有个小坑,github.com/golang/protobuf/protoc-gen-gogoogle.golang.org/protobuf/cmd/protoc-gen-go 是不同的!前者是旧版本,后者是 Google 接管的新版本,他们的 API 是不同的,用于生成的命令,生成的文件都是不一样的。

由于目前的 gRPC-go 源码中的 example 是使用后者的生成方式,所以我们也采用 google 版本。

下面我们通过命令安装两个库(因为这些文件在安装 grpc 时以及下载了,这里只需要install即可)

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

安装后打开你的 $GOPATH/bin 目录下,应该有以下两个 .exe 文件

官网关于go语言的代码生成工具安装文档如下:

Go Generated Code Guide | Protocol Buffers Documentation

3、Proto 文件介绍

文件格式 xxx.proto

1
2
UserService.proto
OrderService.proto

注释

  1. 单行注释 //
  2. 多行注释 /* */

message

message: protobuf 中一个定义消息类型是通过关键字 message 指定的。消息就是需要传输的数据格式的定义

message 关键字类似于 C++/Java 中的 class,C/Go 中的 struct

在消息中承载的数据分别对应每个字段,其中每个字段都有一个名字和一种类型

一个 proto 文件可以定义多个消息类型,如

1
2
3
4
message User {
string username = 1;
int64 age = 2;
}

字段规则

  • required: 消息体中必填字段。不设置会导致编码异常。在 protobuf2 中使用,在 protobuf3 中被删去。
  • optional: 消息体中可选字段。protobuf3 中没有了 reuired,optional 等说明关键字,都默认为 optional。
  • repeated: 消息体中可重复字段。重复的值的顺序会被保留,在go中重复的会被定义为切片。在 python 中被定义为 list
  • oneof: 消息体中只设置多个字段的一个字段。类似 enum。

字段映射

.proto Type Notes C++ Type Python Type Go Type
double double float float64
float float float float32
int32 使用变长编码,对于负值的效率很低,如果你的域有 可能有负值,请使用sint64替代 int32 int int32
uint32 使用变长编码 uint32 int/long uint32
uint64 使用变长编码 uint64 int/long uint64
sint32 使用变长编码,这些编码在负值时比int32高效的多 int32 int int32
sint64 使用变长编码,有符号的整型值。编码时比通常的 int64高效。 int64 int/long int64
fixed32 总是4个字节,如果数值总是比总是比228大的话,这 个类型会比uint32高效。 uint32 int uint32
fixed64 总是8个字节,如果数值总是比总是比256大的话,这 个类型会比uint64高效。 uint64 int/long uint64
sfixed32 总是4个字节 int32 int int32
sfixed32 总是4个字节 int32 int int32
sfixed64 总是8个字节 int64 int/long int64
bool bool bool bool
string 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文 本。 string str/unicode string
bytes 可能包含任意顺序的字节数据。 string str []byte

默认值

protobuf3 删除了 protobuf2 中用来设置默认值的 default 关键字,为各类型定义的默认值

类型 默认值
bool false
整型 0
string 空字符串””
枚举enum 第一个枚举元素的值,因为Protobuf3强制要求第一个枚举元素的值必须是0,所以枚举的默认值就是0;
message 不是null,而是DEFAULT_INSTANCE

消息号

在消息体的定义中,每个字段都必须要有一个唯一的标识号,标识号是 [1, 2^29-1] 范围内的一个整数。

形式看上去与”赋值”类似。

嵌套消息

可以在其他消息类型中定义、使用消息类型,在下面的例子中,person 消息就定义在 PersonInfo 消息内

1
2
3
4
5
6
7
8
message PersonInfo{
message Person{
string name = 1;
int32 height = 2;
repeated int32 weight = 3;
}
repeated Person info = 1;
}

如果要在父消息外重用这个消息类型,需要使用 PersonInfo.Person 的形式来使用它,如:

1
2
3
message PersonMessage{
PersonInfo.Person info = 1;
}

服务定义

如果要将消息类型用在 RPC 系统,可以在 .proto 文件中定义一个 RPC 服务接口,protocol buffer 编译器将会根据所选择的不同语言生成服务接口代码及存根。

1
2
3
4
service SearchService{
# rpc 服务函数名 (参数) 返回 (返回参数)
rpc Search(SearchRequest) returns (RequestResponse)
}

上述代码表示,定义了一个rpc服务方法,该方法接收参数为 SearchRequest 返回 RequestResponse

import 使用

import 用于导入其它的 proto 文件

1
2
// 从执行protoc这个命令的当前目录开始算起
import "user.proto";

any 任意类型

需要导入 any.proto,属性使用 google.protobuf.Any 定义

1
2
3
4
5
import "google/protobuf/any.proto";

message HelloAny {
google.protobuf.Any data = 1;
}

结构体中的类型:

1
Data *anypb.Any `protobuf:"bytes,1,opt,name=data,proto3" json:"data,omitempty"`

4、Proto 文件示例

proto 文件内容如下(可以当作模板记下来)

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
// 使用proto3语法,有2和3
syntax = "proto3";

// 和 go 相关的内容
// option go_package = "path;name";
// path表示生成的go文件的存放地址,会自动生成目录,.表示当前目录
// name表示生成的go文件的包名
// 生成的go文件处于哪个目录哪个包中
// 这里声称在当前目录,service包中
option go_package = ".;service";


// 和 java 相关的内容
// 后续protobuf生成的java代码 一个源文件还是多个源文件 xx.java
option java_multiple_files = false;
// 指定protobuf生成的类 放置在哪个包中
option java_package = "com.suns";
// 指定的protobuf生成的外部类的名字(管理内部类【内部类才是真正开发使用】)
option java_outer_classname = "UserServce";

// 我们需要定义一个服务,在服务中需要有一个方法,这个方法可以接收客户端参数,返回服务端响应
// 其实很容易可以看出,我们定义了一个service,称为SayHello,这个服务有一个rpc方法,名为SayHello
// 这个方法会发送一个HelloRequest返回一个HelloResponse
service SayHello {
rpc SayHello(HelloRequest) returns (HelloResponse) {}
}

// message关键字,可以理解为结构体
// 这个比较特别的是变量后的"赋值"(这里并不是赋值,而是定义这个变量在message中的位置)
message HelloRequest {
string requestName = 1;
// int64 age = 2;
}

message HelloResponse {
string responseMsg = 1;
}

接下来可以通过 protoc 生成对应语言的代码,打开 Terminal,进入 proto 目录,输入一些代码即可

两个命令(当作模板可以记下)

1
2
3
4
5
protoc --go_out=. hello.proto     // 如果想输出其他语言的文件,请使用对应的参数
protoc --go-grpc_out=. hello.proto

protoc -I internal/service/pb internal/service/pb/*.proto --go_out=.
protoc -I internal/service/pb internal/service/pb/*.proto --go-grpc_out=.

系统会根据 go_out 指定的目录再拼接 proto 文件中 go_package 指定的目录生成对应的包名

输入完可以发现在proto目录下生成了两个文件,我们使用时只需要重写或修改其中的我们定义的方法,加上业务逻辑即可


02-protocol buffer
https://flepeng.github.io/046-gRPC-02-protocol-buffer/
作者
Lepeng
发布于
2024年4月1日
许可协议