Protobuf是Google开源的一个项目,博主将会在几篇博文中对其进行讲解。
本文实例源码github地址:https://github.com/yngzMiao/yngzmiao-blogs/tree/master/2019Q4/20191225。
有些时候,我们希望将对象保存到文件中,或者传输给其他的应用程序。比如:web网页后端和前端的数据交互,应用程序产生的中间数据等等。
这种将数据结构或对象以某种格式转化为字节流的过程,称之为序列化
(Serialization),目的是把当前的状态保存下来,在需要时复原数据结构或对象(序列化时不包含与对象相关联的函数,所以后面只提数据结构)。反序列化
(Deserialization),是序列化的逆过程,读取字节流,根据约定的格式协议,将数据结构复原。
在序列化和反序列化的过程中,需要注意的点:
铺开
,然后串接在一起;由上可见,格式协议
是最重要的,它直接决定了序列化和反序列化的效率、字节流的大小和可读性等。
最常见的序列化和反序列化的格式就是json格式、XML格式,他们有自己的一套完整的格式协议。而本文讲解的格式是Protobuf
。
先贴上Protobuf的官方资料:
Protobuf
是Google
开发的一种用于序列化结构化数据(比如Java中的Object,C中的Structure)的语言中立、平台中立、可扩展的数据描述语言,可用于数据存储、通信协议等方面。Protocol Buffers可以理解为是更快、更简单、更小的JSON或者XML,区别在于Protocol Buffers是二进制格式,而JSON和XML是文本格式。
目前protobuf支持的语言包括:C++、C#、Java、JS、OC、PHP、Ruby这七种。
相比较于XML、json,Protobuf的优点:
相对而言,Protobuf也有缺点:
既然Protobuf可以自动生成数据访问类
,也就是说,只要规定了.proto
文件,可以直接生成C++的.cc文件和.h文件,可以直接生成python的.py文件,可以直接生成Java的.java文件……
那么它是如何生成的呢?这就需要下载安装Protobuf了。
Protobuf的release版本,下载可以移步:Protobuf release版本。
如果是Linux操作系统下,可以直接下载:protoc-3.8.0-linux-x86_64.zip。
这个版本包含了protoc二进制文件以及与protobuf一起分发的一组标准.proto文件。
进入bin文件夹,查看protoc的版本信息:
./protoc --version
如果打印出了protoc的版本信息,就表示没有任何问题。
当然,你也可以选择下载Protobuf的源代码,然后通过解压、编译、安装来使用它。这种方式的下载路径为:protobuf-3.8.0.tar.gz。
python2安装步骤如下所示:
tar -xzf protobuf-3.8.0.tar.gz
cd protobuf-3.8.0
./configure --prefix=$INSTALL_DIR
make
make check
make install
python3安装步骤如下所示:
tar -xzf protobuf-3.8.0.tar.gz
cd protobuf-3.8.0
./autogen.sh
./configure
make
make check
sudo make install
sudo ldconfig # refresh shared library cache.
很有可能,执行过程中会出现以下错误提示:
./autogen.sh: 4: ./autogen.sh: autoreconf: not found
解决办法:执行以下命令即可。
sudo apt-get install autoconf
sudo apt-get install automake
sudo apt-get install libtool
其实推荐第一种安装方式,在protobuf的使用过程中,一般只需要可执行文件即可。但是,如果你使用C++版本,但自己没有Google对应的protobuf头文件和静态库,还是需要第二种安装方式。
通常情况Protobuf都安装在/usr/local
目录下,该目录下包含了Protobuf的头文件,静态库和动态库文件,如果是需要使用C++版本,头文件和静态库需要拷贝出来。
在使用Protobuf之前,需要提前创建一个.proto文件。在.proto文件中,需要定义要生成的数据访问类的成员信息等内容。然后,就可以指定该.proto文件来生成了。
如何直接生成呢?这就需要用到安装的可执行文件了。
./protoc 指定.proto文件 --cpp_out=./
./protoc 指定.proto文件 --java_out=./
./protoc 指定.proto文件 --python_out=./
每个定义结构化数据结构体的.proto文件,也需要按照一定的结构和语法进行编写,但这个语法是非常简单的。
提前声明一点,proto2和proto3的语法规则是有一定的差异和不兼容性的,需要注意。
在编写.proto文件的最开始部分,需要指定.proto文件版本:
syntax = "proto2"; //声明proto2版本(选其一)
syntax = "proto3"; //声明proto3版本(选其一)
使用message定义一个消息类型,与C++、Java等高级语言对应起来就可以理解为Class
。
每个message通常由字段修饰符、字段类型、字段名、标识号组成。
以Person为例,在proto2
中:
message Person {
required int32 id =1;
required string name = 2;
optional int32 age = 3;
repeated string email = 4;
}
字段修饰符:只有三种字段修饰符(required、optional、repeated),且每个字段必须有字段修饰符。
[default=xxx]
指定一个默认值,若没有显示指定默认值并且该字段没有被设置,则会使用该类型的默认值;[packed=true]
来保证更高效的编码。字段类型:可以指定proto定义的数据类型,当然也可以指定自己定义的数据类型。proto定义的数据类型如下:
.proto类型 | C++ Type | Java Type | Python Type | Note |
---|---|---|---|---|
double | double | double | float | |
float | float | float | float | |
int32 | 使用可变长度编码,负数编码效率低下。如果可能具有负值,请改用sint32 | int32 | int | int |
int64 | 使用可变长度编码,负数编码效率低下。如果可能具有负值,请改用sint64 | int64 | long | int/long |
uint32 | 使用可变长度编码 | uint32 | int | int/long |
uint64 | 使用可变长度编码 | uint64 | long | int/long |
sint32 | 使用可变长度编码 | int32 | int | int |
sint64 | 使用可变长度编码 | int64 | long | int/long |
fixed32 | 始终为四个字节 | uint32 | int | int/long |
fixed64 | 始终为八个字节 | uint64 | long | int/long |
sfixed32 | 始终为四个字节 | int32 | int | int |
sfixed64 | 始终为八个字节 | int64 | long | int/long |
bool | bool | boolean | bool | |
string | 字符串必须始终包含UTF-8编码或7位ASCII文本 | string | String | unicode(Python 2)/str(Python 3) |
bytes | 可以包含任意字节序列 | string | ByteString | bytes |
标识号:在消息定义中,每个字段都有唯一
的一个标识符。这些标识符是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变。
注:[1, 15]之内的标识号在编码的时候会占用一个字节。[16, 2047]之内的标识号则占用2个字节。所以应该为那些频繁出现的消息元素保留 [1, 15]之内的标识号。切记:要为将来有可能添加的、频繁出现的标识号预留一些标识号。
但是,在proto3
中,对这些规则做了一些的修改:
为什么移除default选项?
在proto3
中,字段的默认值只能根据字段类型由系统决定。也就是说,默认值全部是约定好的,而不再提供指定默认值的语法。
在proto2
中,若某字段被设置为默认值的时候,该字段不会被序列化。这样可以节省空间,提高效率。但这样就无法区分某字段是根本没赋值,还是赋值了默认值。也就是说,如果更新default默认值,会出现意想不到的问题。
为什么取消required字段修饰符?
因为required是永久性
的:在将一个字段标识为required的时候,应该特别小心。如果在某些情况下不想写入或者发送一个required的字段,将原始该字段修饰符更改为optional可能会遇到问题——旧版本的使用者会认为不含该字段的消息是不完整的,从而可能会无目的的拒绝解析。
Google的一些工程师得出了一个结论:使用required弊多于利;他们更愿意使用optional和repeated而不是required。当然,这个观点并不具有普遍性。
也就是说,在proto3
中,定义同样的message需要这样:
message Person {
string name = 1;
string phone = 2;
string email = 3;
repeated string address = 4;
}
当然,除了proto定义的数据类型之外,还可以指定自己定义的数据类型,甚至是枚举类型。
自己定义新的数据类型,只需要在.proto文件中定义新的message类型。枚举类型利用enum
开头,需要注意枚举类型的第一个字段的标识号必须为0。
例如,在proto2中:
syntax = "proto2";
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2;
}
message Address {
optional string country = 1;
optional string detail = 2;
}
message Person {
required int32 id =1;
required string name = 2;
optional int32 age = 3;
repeated string email = 4;
repeated PhoneNumber phone = 5;
optional Address address = 6;
}
在proto3中:
syntax = "proto3";
enum PhoneType {
MOBILE = 0;
HOME = 1;
WORK = 2;
}
message PhoneNumber {
string number = 1;
PhoneType type = 2;
}
message Address {
string country = 1;
string detail = 2;
}
message Person {
int32 id =1;
string name = 2;
int32 age = 3;
repeated string email = 4;
repeated PhoneNumber phone = 5;
Address address = 6;
}
proto可以通过导入import其他.proto文件中的定义来使用它们。即:
import proto路径
proto可以新增一个可选的package声明符,用来防止不同的消息类型有命名冲突。即:
package com.yngzmiao;
当然,proto除了这些定义规则之外,还有其他的规则。如message嵌套定义
、RPC服务接口
等等,一般情况下也不会使用到。需要了解的可以参考官方文档。