【序列化和反序列化】Protobuf
1. 简介
protocol buffers(ProtoBuf)是一种语言无关、平台无关、可扩展的序列化结构数据的方法,它可用于(数据)通信协议、数据存储等。
protocol buffers是一种灵活、高效、自动化机制的结构数据序列化方法-可类比XML,但是比XML更小(3~10倍),更快(20~100倍)。json/xml都是基于文本格式,protobuf是二进制格式。
2. 使用ProtoBuf的例子
2.1 创建 .proto 文件,定义数据结构
使用ProtoBuf ,首先需要通过ProtoBuf语法定义数据结构(消息),这些定义好的数据结构保存在.proto为后缀的文件中。
例子:
文件名:score.proto
// 指定protobuf的版本,proto3是最新的语法版本
syntax = "proto3";
// 定义数据结构,message 你可以想象成java的class,c语言中的struct
message Score {
string className = 1; // 定义一个string类型的字段,字段名字为className, 序号为1
string stuName = 2; // ...
string course = 3;
int32 score = 4;
}
说明:proto文件中,字段后面的序号,不能重复,可以理解成字段的唯一ID
2.2 安装ProtoBuf编译器
protobuf的github发布地址:https://github.com/protocolbuffers/protobuf/releases
protobuf的编译器叫protoc,在上面的网址中找到最新版本的安装包,下载安装。
这里下载的是:protoc-3.9.1-win64.zip , windows 64位系统版本的编译器,下载后,解压到你想要的安装目录即可。
提示:安装完成后,将 [protoc安装目录]/bin 路径添加到PATH环境变量中
打开cmd,命令窗口执行protoc命令,没有报错的话,就已经安装成功。
2.3 将.proto文件,编译成指定语言类库
protoc编译器支持将proto文件编译成多种语言版本的代码,我们这里以java为例。
切换到proto文件所在的目录,执行下面命令
protoc --java_out=. score.proto
然后在当前目录生成了一个ScoreOuterClass.java的java类文件,这个就是我们用protobuf语法定义的数据结构对应的java类文件,通过这个类文件文件我们就可以操作定义的数据结构。
2.4 在代码中使用ProtoBuf对数据进行序列化和反序列化
因为上面的例子使用的是java,我们先导入protobuf的基础类库。
maven:
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>3.9.1</version>
</dependency>
使用ProtoBuf的例子
package com.chenly.serialize.protobuf;
import com.chenly.serialize.bean.ScoreOuterClass;
/**
*
* @author: chenly
* @date: 2022-11-28 16:41
* @description:
* @version: 1.0
*/
public class protobufTest {
public static void main(String[] args) {
ScoreOuterClass.Score.Builder builder = ScoreOuterClass.Score.newBuilder();
// 设置字段值
builder.setClassName("实验班");
builder.setScore(70);
ScoreOuterClass.Score score = builder.build();
// 序列化,将数据根据protobuf格式,转化为字节数组,
byte[] byteArray = score.toByteArray();
// 反序列化
try {
ScoreOuterClass.Score newResponse =ScoreOuterClass.Score.parseFrom(byteArray);
System.out.println(newResponse.getClassName());
System.out.println(newResponse.getScore());
} catch (Exception e) {
e.printStackTrace();
}
}
}
3. protobuf定义消息
消息(message) , 在protobuf中指的就是我们要定义的数据结构
3.1 语法
syntax = "proto3";
message 消息名 {
消息体
}
syntax关键字定时使用的是proto3语法版本,如果没有指定默认使用的是proto2。
message关键词,标记开始定义一个消息。消息体,用于定义各种字段类型。
例子:
syntax = "proto3";
message Score {
string className = 1; //定义一个string类型字段
string stuName = 2;
string course = 3;
int32 score = 4;
}
定义了一个Score 消息,这个消息有4个字段,className、stuName、course是字符串类型,score是int32类型。
支持双斜杠(//)语法格式的注释。
提示:我们通常将protobuf消息定义保存在.proto为后缀的文件中。
3.2 分配标识号
通过前面的例子,在消息定义中,每个字段后面都有一个唯一的数据,这个就是标识号。
这些标识号是用来在消息的二进制格式中识别各个字段的,一旦开始使用就不能够再改变,每个消息内唯一即可,不同的消息定义可以拥有相同的标识号。
注意:[1,15]之内的标识号在编码的时候会占用一个字节。[16,2047]之内的标识号则占用2个字节。所以应该为哪些频繁出现的消息元素保留[1,15]之内的标识号。切记:要为将来有可能添加的、频繁出现的字段预留一些标识号。
保留标识号(reserved) 如果你想保留一些标识符,留给以后用,可以使用下面语法
message Score {
reserved 2, 15, 9 to 11; // 保留2,15,9到11这些标识号
}
如果使用了这些保留的标识号,protocol buffer编译器会输出警告信息
3.3 为消息定义包
我们也可以为消息定义包
例子:
package com.chenly.protobuf;
message Score { ... }
定义了一个包 com.chenly.protobuf
3.4 将消息编译成各种语言版本的类库
编译器命令格式:
protoc [OPTION] PROTO_FILES
OPTION是命令的选项,PROTO_FILES是我们要编译的proto消息定义文件,支持多个
常用OPTION选项:
--cpp_out=OUT_DIR 指定代码生成目录,生成 C++ 代码
--csharp_out=OUT_DIR 指定代码生成目录,生成 C# 代码
--java_out=OUT_DIR 指定代码生成目录,生成 java 代码
--js_out=OUT_DIR 指定代码生成目录,生成 javascript 代码
--objc_out=OUT_DIR 指定代码生成目录,生成 Objective C 代码
--php_out=OUT_DIR 指定代码生成目录,生成 php 代码
--python_out=OUT_DIR 指定代码生成目录,生成 python 代码
--ruby_out=OUT_DIR 指定代码生成目录,生成 ruby 代码
例子:
protoc --java_out=. demo.proto
编译demo.proto消息,在当前目录导出java版本的代码
3.5 选项
下面是一些常用的选项:
- java_package单独为java定义包名字
- java_outer_classname单独为java定义生成的类名
3.6 字段类型
支持多种数据类型,例如:string、int32、double、float等等
下表罗列出了protobuf类型和其他语言类型的映射表
.proto Type | Java Type | Notes |
double | double | |
float | float | |
int32 | int | 使用变长编码,对于负值的效率很低,如果你的域有可能有负值,请使用sint64替代 |
uint32 | int | 使用变长编码 |
uint64 | long | 使用变长编码 |
sint32 | int | 使用变长编码,这些编码在负值时比int32高效的多 |
sint64 | long | 使用变长编码,有符号的整型值。编码时比通常的int64高效 |
fixed32 | int | 总是4个字节,如果数值总是比总是比228大的话,这个类型会比uint32高效 |
fixed64 | long | 总是8个字节,如果数值总是比总是比256大的话,这个类型会比uint64高效 |
sfixed32 | int | 总是4个字节 |
sfixed64 | long | 总是8个字节 |
bool | boolean | |
string | String | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 |
bytes | ByteString | 可能包含任意顺序的字节数据 |
protobuf枚举类型
例子:
syntax = "proto3";//指定版本信息,不指定会报错
enum PhoneType //枚举消息类型,使用enum关键词定义,一个电话类型的枚举类型
{
MOBILE = 0; //proto3版本中,首成员必须为0,成员不应有相同的值
HOME = 1;
WORK = 2;
}
// 定义一个电话消息
message PhoneNumber
{
string number = 1; // 电话号码字段
PhoneType type = 2; // 电话类型字段,电话类型使用PhoneType枚举类型
}
protobuf数组类型
在protobuf消息中定义数组类型,是通过在字段前面增加repeated关键字实现,标记当前字段是一个数组。
1. 整数数组的例子
message Msg {
// 只要使用repeated标记类型定义,就表示数组类型。
repeated int32 arrays = 1;
}
2. 字符串数组
message Msg {
repeated string names = 1;
}
protobuf消息嵌套
我们在各种语言开发中类的定义是可以互相嵌套的,也可以使用其他类作为自己的成员属性类型。
在protobuf中同样支持消息嵌套,可以在一个消息中嵌套另外一个消息,字段类型可以是另外一个消息类型。
1. 引用其他消息类型的用法
// 定义Result消息
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3; // 字符串数组类型
}
// 定义SearchResponse消息
message SearchResponse {
// 引用上面定义的Result消息类型,作为results字段的类型
repeated Result results = 1; // repeated关键词标记,说明results字段是一个数组
}
2.消息嵌套
类似类嵌套一样,消息也可以嵌套
例子:
message SearchResponse {
// 嵌套消息定义
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3;
}
// 引用嵌套的消息定义
repeated Result results = 1;
}
3. import导入其他proto文件定义的消息
我们在开发一个项目的时候通常有很多消息定义,都在写一个proto文件,不方便维护,通常会将消息写在不同的proto文件中,在需要的时候可以通过import导入其他proto文件定义的消息。
例子:
保存文件: result.proto
syntax = "proto3";
// Result消息定义
message Result {
string url = 1;
string title = 2;
repeated string snippets = 3; // 字符串数组类型
}
保存文件: search_response.proto
syntax = "proto3";
// 导入Result消息定义
import "result.proto";
// 定义SearchResponse消息
message SearchResponse {
// 使用导入的Result消息
repeated Result results = 1;
}
protobuf map类型
1. map语法
map<key_type, value_type> map_field = N; //key_type 可以是任何整数或字符串类型
2.map的例子
syntax = "proto3";
message Product
{
string name = 1; // 商品名
// 定义一个k/v类型,key是string类型,value也是string类型
map<string, string> attrs = 2; // 商品属性,键值对
}
map字段不能使用repeated关键字修饰