文章目录
- 01protobuf基础
- protobuf概述
- message
- 定义message结构
- 保留Filed和保留Filed number
- 枚举类型
- 引用其它message类
- message扩展
- 数据类型对应关系
- 编码规则
- 可变长整数编码
- 有符号整数编码
- 定长编码
- 代码生成
- 下载安装protobuf
- 生成代码
- 方法1:使用cmd
- 方法2:使用java调用cmd
- 使用pom生成java类
- 编译生成Java类
- 使用
- 引入protobuf
- 使用builder
- idea使用protobuf
- 添加protobuf支持
01protobuf基础
protobuf概述
protobuf是google团队开发的用于高效存储和读取结构化数据的工具。什么是结构化数据呢,正如字面上表达的,就是带有一定结构的数据。比如电话簿上有很多记录数据,每条记录包含姓名、ID、邮件、电话等,这种结构重复出现。
xml、json也可以用来存储此类结构化数据,但是使用protobuf表示的数据能更加高效,并且将数据压缩得更小,大约是json格式的1/10,xml格式的1/20。
它是一种轻便高效的数据格式,平台无关、语言无关、可扩展,可用于通讯协议和数据存储等领域。
- 优点
- 平台无关,语言无关,可扩展;
- 提供了友好的动态库,使用简单;
- 解析速度快,比对应的XML快约20-100倍;
- 序列化数据非常简洁、紧凑,与XML相比,其序列化之后的数据量约为1/3到1/10。
- 缺点
- 不适合用于对基于文本的标记文档(如HTML)建模,因为文本不适合描述数据结构
- 通用性较差:Json, XML已经成为多种行业标准的编写工具,而Protobuf只是Google公司内部使用的工具
- 自解释性差:以二进制数据流方式存储(不可读) ,需要通过.proto文件才能了解到数据结构
message
定义message结构
protobuf使用message,类似class文件,
例如:
message Person {
// ID(必需)
required int32 id = 1;[default = 0]
// 姓名(必需)
required string name = 2;
// email(可选)
optional string email = 3;[default = ""]
// 朋友(集合)
repeated string friends = 4 [packed=true];
}
其中Person是message
这种结构的名称,name、id、email是其中的Field,每个Field保存着一种数据类型,后面的1、2、3是Filed对应的数字id。id在115之间编码只需要占一个字节,包括Filed数据类型和Filed对应数字id,在162047之间编码需要占两个字节,所以最常用的数据对应id要尽量小一些。
optional
后面可以加default默认值,如果不加,数据类型的默认为0[default = 0],字符串类型的默认为空串[default = “”]。
repeated
后面加[packed=true]会使用新的更高效的编码方式。
注意:使用required规则的时候要谨慎,因为以后结构若发生更改,这个Filed若被删除的话将可能导致兼容性的问题。
syntax = "proto3";
package net.cc.luffy.entity.proto;//指定java的包名,生成java之后的包路径
//option java_package = "net.cc.luffy.entity.proto"; \\指定java的报名
option java_outer_classname = "UpDownProto";//指定java的编译前类名,生成java之后,java文件交
// 起降记录
message UpDown {
// 起降记录ID
fixed64 id = 1;
// 设备ID
string deviceId = 2;
// 用户ID
fixed64 usrId = 3;
// 厂商ID
string mid = 4;
// 起飞时间
fixed64 upTime = 5;
// 降落时间
fixed64 downTime = 6;
// 飞行状态
int32 flyStatus = 7;
// 是否删除
bool isDelete = 8;
// 日志跟踪ID
string traceId = 9;
// 创建时间
fixed64 createdate = 10;
// 平均速度
double avgSpeed = 11;
// 平均高度
double avgHeight = 12;
// 最大速度
double maxSpeed = 13;
// 最大高度
double maxHeight = 14;
// 最小速度
double minSpeed = 15;
// 最小高度
double minHeight = 16;
// 开关机记录ID
fixed64 onOffId = 17;
}
保留Filed和保留Filed number
每个Filed对应唯一的数字id,但是如果该结构在之后的版本中某个Filed删除了,为了保持向前兼容性,需要将一些id或名称设置为保留的,即不能被用来定义新的Field。
实质:已经定义的数字id和字段为了兼容性,之后不能重复使用,若是对应的字段删除,就记录下来,以后不再使用
message Person {
reserved 2, 15, 9 to 11;//排除的id
reserved "samples", "email";//排除的字段
}
枚举类型
比如电话号码,只有移动电话、家庭电话、工作电话三种,因此枚举作为选项,如果没设置的话枚举类型的默认值为第一项。
在上面的例子中在个人message中加入电话号码这个Filed。如果枚举类型中有不同的名字对应相同的数字id,需要加入option allow_alias = true
这一项,否则会报错。枚举类型中也有reserverd Filed和number,定义和message中一样。
message Person {
//普通参数
required string name = 1;
required int32 id = 2;
optional string email = 3;
//枚举--手机号类型(移动电话、家庭电话、工作电话)
enum PhoneType {
//allow_alias = true;
MOBILE = 0;
HOME = 1;
WORK = 2;
}
//定义手机号信息
message PhoneNumber {
required string number = 1;
optional PhoneType type = 2 [default = HOME];//默认是家庭电话
}
//手机号字段
repeated PhoneNumber phones = 4;
}
引用其它message类
- 在同一个文件中,可以直接引用定义过的message类型。
- 在一个message类型中嵌套定义其它的message类型
- 在同一个项目中,可以用
import
来导入其它message类型。
import "myproject/other_protos.proto";
message扩展
message Person {
// ...
extensions 100 to 199;//扩展允许的id范围
}
在另一个文件中,import 这个proto之后,可以对Person这个message进行扩展。
extend Person {
optional int32 bar = 126;
}
数据类型对应关系
在使用规则创建proto类型的数据结构文件之后,会将其转化成对应编程语言中的头文件或者类定义。
proto中的数据类型和c++,Python中的数据类型对应规则如下:
.proto | C++ | Python | java | 介绍 |
double | double | float | double | |
float | float | float | float | |
int32 | int32 | int | int | 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint32。 |
int64 | int64 | int/long | long | 使用可变长编码方式。编码负数时不够高效——如果你的字段可能含有负数,那么请使用sint64。 |
uint32 | uint32 | int/long | int | Uses variable-length encoding. |
uint64 | uint64 | int/long | long | Uses variable-length encoding. |
sint32 | int32 | int | int | 使用可变长编码方式。有符号的整型值。编码时比通常的int32高效。 |
sint64 | int64 | int/long | long | 使用可变长编码方式。有符号的整型值。编码时比通常的int64高效。 |
fixed32 | uint32 | int/long | int | 总是4个字节。如果数值总是比总是比228大的话,这个类型会比uint32高效。 |
fixed64 | uint64 | int/long | long | 总是8个字节。如果数值总是比总是比256大的话,这个类型会比uint64高效。. |
sfixed32 | int32 | int | int | 总是4个字节。 |
sfixed64 | int64 | int/long | long | 总是8个字节。 |
bool | bool | bool | boolean | |
string | string | str/unicode | String | 一个字符串必须是UTF-8编码或者7-bit ASCII编码的文本。 |
bytes | string | str | ByteString | 可能包含任意顺序的字节数据。 |
编码规则
protobuf有一套高效的数据编码规则。
可变长整数编码
每个字节有8bits,其中第一个bit是most significant bit(msb),0表示结束,1表示还要读接下来的字节。
对message中每个Filed来说,需要编码它的数据类型、对应id以及具体数据。
数据类型有以下6种,可以用3个bits表示。每个整数编码用最后3个bits表示数据类型。所以,对应id在1~15之间的Filed,可以用1个字节编码数据类型、对应id。
Type | Meaning | Used For | |
0 | Varint | int32, int64, uint32, uint64, sint32, sint64, bool, enum | |
1 | 64-bit | fixed64, sfixed64, double | |
2 | Length-delimited | string, bytes, embedded messages, packed repeated | fields |
3 | Start group | groups (deprecated) | |
4 | End group | groups (deprecated) | |
5 | 32-bit | fixed32, sfixed32, float |
比如对于下面这个例子来说,如果给a赋值150,那么最终得到的编码是什么呢?
message Test {
optional int32 a = 1;
}
首先数据类型编码是000,因此和id联合起来的编码是00001000. 然后值150的编码是1 0010110,采用小端序交换位置,即0010110 0000001,前面补1后面补0,即10010110 00000001,即96 01,加上最前面的数据类型编码字节,总的编码为08 96 01。
有符号整数编码
如果用int32来保存一个负数,结果总是有10个字节长度,被看做是一个非常大的无符号整数。使用有符号类型会更高效。它使用一种ZigZag
的方式进行编码。即-1编码成1,1编码成2,-2编码成3这种形式。
也就是说,对于sint32来说,n编码成 (n << 1) ^ (n >> 31),注意到第二个移位是算法移位。
定长编码
定长编码是比较简单的情况。
代码生成
下载安装protobuf
- 在https://github.com/protocolbuffers/protobuf/releases 下载合适的版本
例如,我下载了protoc-3.9.0-win32.zip - 将解压出来的protoc.exe放在一全英文路径下,并把其路径名放在windows环境变量下的
path
下,同时添加proto_path
,值为protoc.exe的路径
生成代码
方法1:使用cmd
在所使用的proto文件路径下打开cmd窗口执行以下命令
protoc -I=源地址 --java_out=目标地址 源地址/xxx.proto
注:此处生成时会以proto里面注明的java_package为路径完整生成,所以目标地址不必包含java_package及之后的路径,比如: option java_package = "com.test.protocol";
那么就会生成com/test/protocol/XXX.java
- -I选项,主要用于指定待编译的.proto消息定义文件所在的目录,即可能出现的包含文件的路径,该选项可以被同时指定多个。此处指定的路径不能为空,如果是当前目录,直接使用.,如果是子目录,直接使用子目录相对径,如:foo/bar/baz,如果要编译的文件import指定的文件路径为baz/test.proto,那么应这么写-I=foo/bar,而不要一直写到baz。
示例命令如下:
protoc -I=. --java_out=../../../../ beans/*.proto apis/*.proto *.proto
方法2:使用java调用cmd
/**
* protoc.exe
* @author ganhaibin
*
*/
public class GenerateClass {
public static void main(String[] args) {
String protoFile = "person-entity.proto";//
String strCmd = "d:/dev/protobuf-master/src/protoc.exe -I=./proto --java_out=./src/main/java ./proto/"+ protoFile;
try {
Runtime.getRuntime().exec(strCmd);
} catch (IOException e) {
e.printStackTrace();
}//通过执行cmd命令调用protoc.exe程序
}
}
使用pom生成java类
<!--版本-->
<properties>
<grpc.version>1.6.1</grpc.version>
<protobuf.version>3.3.0</protobuf.version>
</properties>
<!--依赖-->
<dependencies>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-netty</artifactId>
<version>${grpc.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-protobuf</artifactId>
<version>${grpc.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>io.grpc</groupId>
<artifactId>grpc-stub</artifactId>
<version>${grpc.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>${protobuf.version}</version>
</dependency>
</dependencies>
<!--build配置-->
<build>
<extensions>
<extension>
<groupId>kr.motd.maven</groupId>
<artifactId>os-maven-plugin</artifactId>
<version>1.5.0.Final</version>
</extension>
</extensions>
<plugins>
<plugin>
<groupId>org.xolstice.maven.plugins</groupId>
<artifactId>protobuf-maven-plugin</artifactId>
<version>0.5.0</version>
<configuration>
<protocArtifact>com.google.protobuf:protoc:${protobuf.version}:exe:${os.detected.classifier}</protocArtifact>
<pluginId>grpc-java</pluginId>
<pluginArtifact>io.grpc:protoc-gen-grpc-java:${grpc.version}:exe:${os.detected.classifier}</pluginArtifact>
</configuration>
<executions>
<execution>
<goals>
<goal>compile</goal>
<goal>compile-custom</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
编译生成Java类
- 在
src/main/proto
文件夹中添加protobuf的文件; - 右键
proto
,将文件夹设置为源码目录 - 点击
maven projects
中Plugins
–protobuf
–protobuf:compile
- 即可在
target/generated-sources/protobuf
中看到根据.proto文件生成的Java类 ;
使用
引入protobuf
- maven项目,引入pom依赖
<dependency>
<groupId>com.google.protobuf</groupId>
<artifactId>protobuf-java</artifactId>
<version>2.5.0</version>
</dependency>
- 普通项目需要引入protobuf-java-2.5.0.jar文件
使用builder
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.util.List;
public class Main {
public static void main(String[] args) throws IOException {
// 按照定义的数据结构,创建一个Person
PersonMsg.Person.Builder personBuilder = PersonMsg.Person.newBuilder();
personBuilder.setId(1);
personBuilder.setName("叉叉哥");
personBuilder.setEmail("xxg@163.com");
personBuilder.addFriends("Friend A");
personBuilder.addFriends("Friend B");
PersonMsg.Person xxg = personBuilder.build();
// 将数据写到输出流,如网络输出流,这里就用ByteArrayOutputStream来代替
ByteArrayOutputStream output = new ByteArrayOutputStream();
xxg.writeTo(output);
// -------------- 分割线:上面是发送方,将数据序列化后发送 ---------------
byte[] byteArray = output.toByteArray();
// -------------- 分割线:下面是接收方,将数据接收后反序列化 ---------------
// 接收到流并读取,如网络输入流,这里用ByteArrayInputStream来代替
ByteArrayInputStream input = new ByteArrayInputStream(byteArray);
// 反序列化
PersonMsg.Person xxg2 = PersonMsg.Person.parseFrom(input);
System.out.println("ID:" + xxg2.getId());
System.out.println("name:" + xxg2.getName());
System.out.println("email:" + xxg2.getEmail());
System.out.println("friend:");
List<String> friends = xxg2.getFriendsList();
for(String friend : friends) {
System.out.println(friend);
}
}
}
也可以:
序列化:byte[] bytes=personBuilder.build().toByteArray();
反序列化:PersonMsg.Person person=PersonMsg.Person.parseFrom(bytes);
可以直接 person.getId();等操作
注意:protobuf不是json,若是使用json解析会抛出异常
idea使用protobuf
添加protobuf支持
安装插件Protobuf Support