简单介绍
protobuf是谷歌推出的一种序列化协议,在认识protobuf之前,我们需要着重认识一下序列化,序列化和反序列化是什么?有什么用呢?
设想一种情景:现在在内存中有一个Java的对象,他在内存中,只要程序一结束或者电脑一断电,这个对象就消失了。现在需要将这个对象保存到物理硬盘上或者需要通过网络进行传输,那该怎么办呢?答案就是序列化,将一个对象序列化成二进制文件,在需要的时候再将它还原成Java对象,这个二进制文件不仅可以在物理硬盘上进行存储,还能在网络上进行传输。这就是序列化的作用,但是Java中的序列化有一些缺点,一是只能在Java中使用,不能跨语言;二是在进行序列化时,需要保存很多的class类型信息,所以序列化之后的二进制文件很大。
protobuf也是做序列化反序列化这个事情的,只不过,protobuf有许多的优点:跨语言,支持许多主流的言语:如Java、C++、Python……,还跨平台,Windows、Linux、Mac。另外一个重要的特性就是,序列化之后的二进制文件小,用在一些网络性能要求高的地方非常合适。
基本语法
syntax = "proto3"
option java_package = "com.ql.protobuftest.util";
option java_outer_classname = "MultSerializer";
message Teacher{
required string name = 1;
option int32 age = 2;
repeated string number = 3;
// 这是注释
/*这也是注释*/
}
一、非message内容
- syntax 用于指定protobuffer的语法版本,不加默认是2.0版本。这必须是文件第一个非空、非注释的行
- option java_package 表示生成的序列化器的Java包
- option java_outer_classname 用于指定生成的Java的类名
二、message部分
消息结构体跟Java的类很像,一个message就对应着Java中的一个类,每个字段对应了Java中的一个属性。在上面的示例中,Teacher就对一个Teacher类,有三个字段,name,age,number。标注 proto中的数据类型 字段名 = 唯一编号
1. 标注
标注是用来限制字段在进行设置时的要求
- required:表示在设置消息的时候,这个属性是必须要的,而且只能设置一个值。在proto3语法中,已经没有这个标注了
- option:表示在设置消息的时候,这个属性是可选的,可以设置,可以不设置,不设置会有默认值
- repeated:表示可重复的,就是这个属性可以设置多个值(0个到多个),可类比于Java中的数组,但实际上在生成Java类的时候,用的是list。
注意:在proto3语法中,已经没有required和option了,只有singular和repeated,不写默认是singular。
2. protobuffer变量
protobuffer中内置了一些变量,可以和每种语言中变量对应起来,因为protobuffer跨语言,所以可以对应起来,例如:
proto变量 | Java类型 | C++类型 |
float | float | float |
double | double | double |
int32 | int | int32 |
int64 | long | int64 |
…… | …… | …… |
太多久不一一列举了,可以参考:protobuffer官网参考文档 值得一提的是,protobuffer和Java一样,除了内置的基本类型之外,还支持枚举、自定义类型
3. 变量名
变量的命名规则和大多数编程语言的命名规则是一样的,数字、字母、下划线等
4. 属性顺序号
这个编号是唯一的,不能重复的,编号范围 1-536870911,19000-19999之间的数字不能使用(因为是保留数字)。在编译之后,这个数字会代替变量名,这也是protobuffer序列化之后体积更小的原因之一,如果是用json格式,那么name就真的需要存储一个name,但是使用protobuffer(像我上面那么写),name就只需要用一个数字代替。
1-15编号占用一个字节,16-2047占用两个字节,所以对于常用的的字段,应该为其保留1-15号序号。
5. 注释
proto中注释采用和Java类似的风格,双斜线(//)和双斜线加星号(/**/)都可以注释
高级语法
1. 使用自定义类型
可以使用自己定义的message作为变量的类型,例如
message SearchResponse {
repeated Result results = 1;
}
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
2. 嵌套
可以在一个message内部嵌套定义额外的message,例如:
message SearchResponse {
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
repeated Result results = 1;
}
如果想要在别的message内部使用message Result,可以使用_Parent_._Type_语法,例如:
message SomeOtherMessage {
SearchResponse.Result result = 1;
}
另外,可以多层嵌套
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;
}
}
}
2. 枚举
如希望有一种类型,它的值只能是某些固定的值,那么枚举可能再合适不过了。一个枚举类型可以自定义一些值,在为这个类型的变量进行赋值时,只能在自定义的值中进行选择。例如:
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;
}
注意:枚举的第一个选项值必须为0,因为:①必须得有一个0作为枚举类型变量的默认值;②得和proto2语法兼容,proto2语法中枚举第一个选项值必须为0
另外,在枚举中可以将相同的值分配给不同的选项,以表示这是两个相同的选项,只是别名,但是需要在申明,否则会编译不通过
message MyMessage1 {
enum EnumAllowingAlias {
// 表是在本消息里可以使用别名
option allow_alias = true;
UNKNOWN = 0;
STARTED = 1;
RUNNING = 1;
}
}
枚举的常量值必须在32位整数的范围内,而且不推荐使用负值,因为效率很低。
枚举可以定义在一个message内部,只作为当前message内部使用。枚举还可以定义在message外部,当前proto文件内部所有的message都可以使用这个枚举。还可以使用语法_MessageType_._EnumType_在一个消息中声明的enum类型作为另一个消息中的字段类型。
同样,如果直接删除或者注释掉枚举里面的某些选项,那么会出现兼容性问题,所以推荐的做法也是使用reserved进行保留。例如:
enum Foo {
// 保留枚举值,2,15,9到11,40到max
reserved 2, 15, 9 to 11, 40 to max;
// 保留枚举名 “FOO”,“BAR”
reserved "FOO", "BAR";
}
注意,不能在同一个保留语句中混合字段名和字段号
4. map
如果想构建一个map,在proto中是可行的,语法为:map<key_type, value_type> map_field = N,例如:
map<string, Project> projects = 3;
注意:枚举不是一个有效的key,不能使用any作value,使用map还需要满足一些要求
- map的字段名不能重复
- 如果为map提供了一个key,但是没有为其提供value,那么value是依据语言而定的。 In C++, Java, and Python the default value for the type is serialized, while in other languages nothing is serialized.
- Wire format ordering and map iteration ordering of map values is undefined, so you cannot rely on your map items being in a particular order.
- When generating text format for a .proto, maps are sorted by key. Numeric keys are sorted numerically.
- When parsing from the wire or when merging, if there are duplicate map keys the last key seen is used. When parsing a map from text format, parsing may fail if there are duplicate keys.
3. 扩展性
直接删除或者注释某个字段,会让之前在使用该字段的地方出现错误,也就是不向前兼容,所以不建议这么做,推荐的做法是用reserved进行保留。
message Foo {
// 保留字段号2,15,9到11
reserved 2, 15, 9 to 11;
// 保留字段名"foo"、"bar"
reserved "foo", "bar";
}
进行保留之后,更新消息时,新定义字段不能使用继续使用保留的字段号和字段名,但是可以将这个字段是存在的进行使用(不推荐)。注意,不能在同一个保留语句中混合字段名和字段号。
4. 默认值
在进行反序列化时,某个字段没有为其设置值,那么根据其类型会有默认值。
- 对于string,默认值是空字符串
- 对于bytes,默认值是空的bytes
- 对于布尔值,默认值是false
- 对于数值类型,默认值是0
- 对于枚举,默认值是第一个定义的枚举值,该值必须为0。
- 对于复合类型(也就是Java中的引用类型),如果没有设置值,默认值和具体的语言有关。
- 重复的字段默认值为空
注意:如果一个基本类型变量的字段被设置为默认值,这个值将不会参与序列化。
自我提高
1. 如果proto文件太大怎么办?
java是单个类文件不能超过65k,如果proto协议文件过大则会导致失败,解决办法是在头部加上:
option java_multiple_files = true;
3. 属性顺序号为什么不能重复?
因为在序列化和反序列化的时候,消息的字段都是用这个属性顺序号进行代替的,如果重复了,字段区分将会出现问题,所以要求属性顺序号唯一不可重复。
4. protobuffer为什么效率高
- 在进行反序列化时,为什么能将一个二进制信息还原成一个Java对象?为什么能还原成一个Teacher实例,而不是Pig、Dog类型实例呢?这是因为序列化之后的信息中包含了class信息,所以可以反序列化还原。但是使用protobuffer,class信息就不会在序列化后的二进制信息中,而是在编译生成的Java工具类中,所以就节省了空间。
- 字段信息也不会使用实际的文字表示,而是使用了属性顺序号进行代替,例如一个字段"name",需要占用四个字节,但是如果使用属性顺序号"1"进行替代,则只需要一个字节。
- 动态的分配值的空间,例如在Java中,一个int类型的变量需要占用4个字节,无论这个变量实际是否用到了4个字节。但是protobuffer在进行序列化的时候,会根据变量的实际大小,动态的分配空间,所以也节省了空间。
5. 每个消息都去生成一个对应的类吗?如果有成千上万个消息呢?在真实开发中,protobuffer是怎么使用的?
这个占时还不知道,因为网上关于proto的资料还比较少,我上面的东西,大部分都是从proto官网上扣下来的。