这章是接上一章,使用RPC包,序列化中没有详细去讲,因为这一块需要看的和学习的地方很多。并且这一块是RPC中可以说是最重要的一块,也是性能的重要影响因子。今天这篇主要会讲其使用方式。
序列化是系统通信的基础组件,在大数据
、AI框架
和云原生
等分布式系统
中广泛使用。
当对象需要跨进程
、跨语言
、跨节点传输
、持久化
、状态读写
、复制
时,都需要进行序列化
,其性能
和易用性
影响运行效率
和开发效率
。
但是对于序列化框架而言,业内将其分为两类:静态序列化框架,动态序列化框架。
其中各有优缺点:
protobuf
、flatbuffer
、thrift
JDK序列化
、Kryo
、Fst
、Hessian
、Pickle
但是前几天阿里推出了Fury,号称比
JDK快了170倍
,并且兼具静态序列化和动态序列化的优点。这个下一章会讲。目前的gRPC框架
是用protobuf
搭建的。所以咱们先让自己的项目成功运行起来再说。所以这个后面再说。
Protocol Buffers
是google开源
的一种结构数据序列化机制,可跨语言、跨平台。
相比XML
、JSON
、Thrift
等其他序列化格式,Protocol Buffers的序列化和反序列化性能是很高的,且Protocol Buffers序列化后是二进制流,因此数据大小和传输速度是很好的。
所以它非常适合在数据存储
或 RPC 数据交换
的场景下使用。
以下使用手法是翻译自官网
定义一个 .proto
文件
定义一个搜索请求消息格式,其中每个搜索请求都包含:
syntax = "proto3";
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
如果不这样写,protocol buffer编译器将假定你使用 proto2。这个声明必须是文件的第一个非空非注释行。
SearchRequest
消息定义指定了三个字段(名称/值对) ,每个字段表示希望包含在此类消息中的每一段数据。每个字段都有一个名称和一个类型指定字段类型:
page_number
和 result_per_page
和一个字符串query
(上面的例子)proto3语法主要包括:
message
)定义service
)定义一个message内的字段一般包含:
添加更多消息类型:
可以在一个.proto
文件中定义多个消息类型。定义与 SearchRequest 消息类型对应的应答消息格式SearchResponse,就可以将其添加到同一个.proto文件中。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
}
message SearchResponse {
...
}
添加注释:
要给你的.proto
文件添加注释,需要使用C/C++
风格的//
和/* ... */
语法。
保留字段:
.proto
文件,这可能会导致严重的问题,包括数据损坏,隐私漏洞等等。消息定义中的每个字段都有一个 唯一的编号
。
这些字段编号用来在消息二进制格式中标识字段,在消息类型使用后就不能再更改。
注意
:
1
到15
中的字段编号需要一个字节
进行编码,包括字段编号和字段类型16
到2047
的字段编号采用两个字节
应该为经常使用的消息元素保留数字1到15的编号。切记为将来可能添加的经常使用的元素留出一些编号。
可以指定的最小字段数是 1 ,最大的字段数是 2 29 − 1 可以指定的最小字段数是1,最大的字段数是 2^{29}−1 可以指定的最小字段数是1,最大的字段数是229−1
即536,870,911。
也不能使用19000
到19999
,它们是预留给Protocol Buffers
协议实现的。
如果你在你的.proto
文件中使用了预留的编号Protocol Buffers
编译器就会报错。
同样,你也不能使用任何之前保留的字段编号。
消息字段可以是下列字段之一:
singular
: 格式正确的消息可以有这个字段的零个或一个(但不能多于一个)。这是 proto3语法的默认字段规则。repeated
: 该字段可以在格式正确的消息中重复任意次数(包括零次)。重复值的顺序将被保留。确保这种情况不会发生的一种方法是指定已删除字段
的字段编号(和/
或名称
,这也可能导致 JSON 序列化问题)是保留的reserved
如果将来有任何用户尝试使用这些字段标识符,protocol buffer编译器
将发出提示。
message Foo {
reserved 2, 15, 9 to 11;
reserved "foo", "bar";
}
注意:不能在同一个reserved
语句中混合字段名
和字段编号
。
当你使用 protocol buffer 编译器
来运行.proto文件
时,编译器用你选择的语言生成你需要使用文件中描述的消息类型,包括获取和设置字段值,将消息序列化为输出流,以及从输入流解析消息的代码。
C++
来说,编译器会为每个.proto文件生成一个.h文件和一个.cc文件,.proto文件中的每一个消息有一个对应的类。 Java
,编译器生成一个.java 文件,每种消息类型都有一个类,还有一个特殊的 Builder 类用于创建消息类实例。 Kotlin
,除了 Java 生成的代码之外,编译器还生成一个每种消息类型的 .kt 文件,包含一个 DSL,可用于简化消息实例的创建。Python
稍有不同ー Python 编译器为.proto文件中的每个消息类型生成一个带静态描述符的模块,然后与 metaclass 一起使用,在运行时创建必要的 Python 数据访问类。Go
,编译器为文件中的每种消息类型生成一个类型(type)到一个.pb.go 文件。Ruby
,编译器生成一个.rb 文件,其中包含一个包含消息类型的 Ruby 模块。Objective-C
,编译器从每个.proto文件生成一个 pbobjc.h 和 pbobjc.m 文件,.proto文件中描述的每种消息类型都有一个类。C#
,编译器生从每个.proto文件生成一个.cs 文件。.proto文件中描述的每种消息类型都有一个类。Dart
,编译器为文件中的每种消息类型生成一个.pb.dart 文件。数据类型:标量类型
和复合类型
标量类型
当解析消息时,如果编码消息不包含特定的 singular 元素,则解析对象中的相应字段将设置为该字段的默认值。
请注意
,对于标量消息字段,一旦消息被解析,就无法判断字段是显式设置为默认值(例如,是否一个布尔值是被设置为false
)还是根本没有设置: 在定义消息类型时应该牢记这一点。例如,如果你不希望某个行为在默认情况下也发生,那么就不要设置一个布尔值,该布尔值在设置为false
时会开启某些行为。还要注意,如果将标量消息字段设置为默认值,则该值将不会在传输过程中序列化。
复合类型
复合类型包括:枚举、嵌套其他message、Any(Map,Oneof)等
在定义消息类型时,你可能希望其中一个字段只能是预定义的值列表中的一个值。
可以通过在消息定义中添加一个枚举,为每个可能的值添加一个常量来非常简单地完成这项工作。
message SearchRequest {
string query = 1;
int32 page_number = 2;
int32 result_per_page = 3;
enum Corpus {
UNIVERSAL = 0;
WEB = 1;
IMAGES = 2;
LOCAL = 3;
NEWS = 4;
PRODUCTS = 5;
VIDEO = 6;
}
Corpus corpus = 4;
}
Corpus enum
的第一个常量映射为零: 每个 enum
定义必须包含一个常量,该常量映射为零作为它的第一个元素。
你可以通过将相同的值分配给不同的枚举常量来定义别名。为此,你需要将 allow _ alias
选项设置为 true
,否则,当发现别名时,protocol 编译器
将生成错误消息。
内部定义
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.
}
}
注意:枚举的常数必须在32位整数的范围内。
由于枚举值在传输时使用
变长编码
,因此负值效率低,因此不推荐使用。
可以在消息定义中定义枚举,如上面的例子所示,也可以在外面定义——这样就可以在.proto
文件中的消息定义中重用这些枚举。
外部定义
enum Ezarten {
option allow_alias = true; //开启枚举值重复开关
ZARTEN1 = 0;
ZARTEN2 = 1;
ZARTEN3 = 2;
ZARTEN4 = 2; //开启option allow_alias = true后枚举值可以重复
}
//定义一个message类型
message ZartenOne {
string name = 1;
int32 age = 2;
int32 height = 3;
Ezarten ezarten = 4;
}
可以使用_MessageType_._EnumType_
语法,使用在一个消息中声明的enum类型
作为不同消息中的字段类型。
使用消息内的枚举
若枚举定义在内部,其他message要使用这个枚举,可以使用 “message名.枚举名”的形式:
//定义一个message类型
message ZartenOne {
string name = 1;
int32 age = 2;
int32 height = 3;
enum Ezarten {
ZARTEN1 = 0;
ZARTEN2 = 1;
ZARTEN3 = 2;
}
Ezarten ezarten = 4;
}
//定义一个message类型
message ZartenTwo {
string name = 1;
int32 age = 2;
int32 height = 3;
ZartenOne.Ezarten ezarten = 4;
}
当对一个使用了枚举的
.proto文件
运行protocol buffer
编译器的时候,对于Java
,Kotlin
,或C++
生成的代码中将有一个对应的enum
,或者对于Python
会生成一个特殊的EnumDescriptor
类,它被用于在运行时生成的类中创建一组带有整数值的符号常量。
生成的代码可能会受到特定于语言的枚举数限制(单种语言的数量低于千)
反序列化
过程中,不可识别的枚举值将保留在消息中,尽管当消息被反序列化时,这种值的表示方式依赖
于语言。
(如 C++ 和 Go)
的开放枚举类型
的语言中,未知枚举值仅存储为其底层的整数表示形式。闭合枚举类型
(如 Java)
的语言中,枚举中的一个类型将用于表示一个无法识别的值,并且可以使用特殊的访问器访问底层的整数。在这两种情况下,如果消息被序列化,那么不可识别的值仍然会与消息一起被序列化。
你可以使用其他消息类型作为字段类型:
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
如果你希望用作字段类型的消息类型已经在另一个.proto文件中定义了,该怎么办?
你可以通过 import
来使用来自其他.proto
文件的定义。要导入另一个.proto
的定义,你需要在文件顶部添加一个import
语句
import "myproject/other_protos.proto";
默认情况下,只能从直接导入的
.proto
文件中使用定义。但是,有时你可能需要将.proto
文件移动到新的位置。你可以在旧目录放一个占位的.proto
文件使用import public
概念将所有导入转发到新位置,而不必直接移动.proto
文件并修改所有的地方。
import public
依赖项可以被任何导入包含import public
语句的proto
的代码传递依赖。
语法:
import
import public
文件A
中两者可以直接引用它们上一级proto文件B的内容。import
引用文件C,则文件A不能使用文件C的内容;若文件B内使用了import public引用文件C,则文件A可以使用文件C的内容。
类似于编程语言中的类是否可以继承的含义。
文件:new.proto
所有的定义都被移到了这里
文件:old.proto
这是所有客户端都要导入的原型
import public "new.proto";
import "other.proto";
文件:client.proto
你可以使用old.proto
和new.proto
,但是不能使用 other.proto
protocol
编译器使用命令行-I/--proto_path
参数指定的一组目录中搜索导入的文件。如果没有给该命令行参数,则查看调用编译器的目录。
一般来说,你应该将
--proto_path
参数设置为项目的根目录并为所有导入使用正确的名称。
方式1:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
方式2:
要在其父消息类型之外重用此消息类型,通过_Parent_._Type_使用。
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
方式3:一层又一层嵌入其中
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;
}
}
}
在不破坏任何现有代码的情况下更新消息类型非常简单:只需记住以下规则:
默认值
,以便新代码能够正确地与旧代码生成的消息交互。OBSOLETE_
“前缀,或者声明字段编号为reserved
,以便.proto
的未来用户不可能不小心重复使用这个编号。int32
、 uint32
、 int64
、 uint64
和 bool
都是兼容的——这意味着你可以在不破坏向前或向后兼容性的情况下将一个字段从这些类型中的一个更改为另一个。sint32
和 sint64
相互兼容,但与其他整数类型不兼容string
和bytes
是兼容的,只要字节是有效的 UTF-8
嵌入的消息与bytes兼容
fixed32
与 sfixed32
兼容 fixed64
与 sfixed64
兼容。string
、bytes
和消息字段
,optional
字段与repeated
字段兼容。
基本类型
字段,期望该字段为可选字段的客户端将接受最后一个输入值消息类型
字段,则合并所有输入元素bools
和 enums
通常是不安全的。
packed
的格式序列化,如果是optional
字段,则无法正确解析这些字段Enum
在格式方面与 int32
、 uint32
、 int64
和 uint64
兼容(请注意,如果不适合,值将被截断)。
proto3
enum
将保留在消息中,但是当消息被反序列化时,这种类型的表示方式依赖于语言。Int
字段总是保留它们的值。oneof
成员是安全的,并且二进制兼容。
多个字段
移动到新的oneof字段
中可能是安全的。未知字段是格式良好的协议缓冲区序列化数据,表示解析器不识别的字段。
当旧二进制
解析由新二进制发送的带有新字段
的数据时,这些新字段
将成为旧二进制中的未知字段
。
在3.5版本
中,我们重新引入了未知字段的保存来匹配 proto2
行为。在3.5及以后的版本中,解析期间保留未知字段,并将其包含在序列化输出中
Any
消息类型允许你将消息作为嵌入类型使用,而不需要其.proto
定义。Any
包含一个任意序列化的字节消息,以及一个解析为该消息的类型作为消息的全局唯一标识符的URL
要使用Any
类型,需要导入google/protobuf/any.proto
给定消息类型的默认类型 URL
是type.googleapis.com/_packagename_._messagename_
import "google/protobuf/any.proto";
message ErrorStatus {
string message = 1;
repeated google.protobuf.Any details = 2;
}
不同的语言实现将支持运行库助手以类型安全的方式打包和解包 Any值:
在java中,Any类型会有特殊的pack()和unpack()访问器,
在C++中,Any类型会有特殊的PackFrom()和UnpackTo()方法。
如果你有一条包含多个字段的消息,并且最多
同时设置其中一个字段,那么你可以通过使用oneof
来实现并节省内存,优化
oneof
字段类似于常规字段,只不过oneof
中的所有字段共享内存,而且最多可以同时设置一个字段。
设置其中的任何成员都会自动清除所有其他成员。
根据所选择的语言,可以使用特殊 case()
或 WhichOneof()
方法检查 oneof
中的哪个值被设置
在生成的代码中,其中一个字段具有与常规字段相同的 getter
和 setter
你还可以获得一个特殊的方法来检查其中一个设置了哪个值
message SampleMessage {
oneof test_oneof {
string name = 4;
SubMessage sub_message = 9;
}
}
然后将其中一个字段添加到该字段的定义中。
你可以添加任何类型的字段,除了map字段
和repeated
字段
oneof有很多特性,具体的个人建议去看文档:
SampleMessage message;
SubMessage* sub_message = message.mutable_sub_message();
message.set_name("name"); // 删除name
sub_message->set_... // 这里的崩溃
Swap()
两个 oneof
消息,每个消息,两个消息将拥有对方的值SampleMessage msg1;
msg1.set_name("name");
SampleMessage msg2;
msg2.mutable_sub_message();
msg1.swap(&msg2);
CHECK(msg1.has_sub_message());
CHECK(msg2.has_name());
添加或删除一个字段时要小心。
one of
的值返回None
/NOT_SET
,这可能意味着 one of
没有被设置,或者它已经被设置为one of
的不同版本中的一个字段。标签重用问题:
移入
或移出
oneof
:在序列化和解析消息之后,你可能会丢失一些信息(某些字段将被清除)。但是,你可以安全地将单个字段移动到新的 oneof
字段中,并且如果已知只设置了一个字段,则可以移动多个字段。oneof
字段再添加回来:这可能会在消息被序列化和解析后清除当前设置的 oneof 字段。oneof
:这与移动常规字段有类似的问题。如果你想创建一个关联映射作为你数据定义的一部分。
map
map_field = N;
key_type
可以是任何整型或字符串类型(除了浮点类型和字节以外的任何标量类型)value_type
可以是除另一个映射以外的任何类型。map<string, Project> projects = 3;
.proto
生成文本格式时,映射按键排序。数字键按数字排序。C++
、 Java
、 Kotlin
和 Python
中,类型的默认值是序列化的,而在其他语言中,没有任何值是序列化的。proto3
支持 JSON
的规范编码,使得系统之间更容易共享数据。下表按类型逐一描述了编码。
如果 json 编码
的数据中缺少某个值,或者该值为 null,那么在解析为protocol buffer
时,该值将被解释为适当的默认值。如果一个字段在 protocol buffer
中具有默认值,为了节省空间,默认情况下 json
编码的数据中将省略该字段。具体实现可以提供在JSON编码
中可选的默认值。
在proto3 JSON 输出中
,值为默认值的字段被省略。可以提供一个选项,用默认值覆盖此行为和输出字段。在缺省情况下
,Proto3 JSON 解析器应该拒绝未知字段,但在解析过程中可能会提供一个忽略未知字段的选项。默认情况下
,proto3 JSON 打印机应该将字段名转换为 lowerCamelCase
,并使用它作为 JSON 名称
。
JSON
名。需要协议3 JSON 解析器同时接受转换后的
lowerCamelCase `名称和原始字段名称。在 JSON 输出中
,默认情况下使用枚举值的名称。可以提供一个选项来代替使用枚举值的数值。剩下的可以看:官方文档