什么是protobuf
它是一个对象序列化/反序列化的工具,什么是对象的序列化/反序列化?就是把一个Java堆中存活的对象转换成一串二进制编码,然后该编码可以用于本地存储和网络传输。反序列化就是根据一串二进制编码还原出原来的那个对象,protobuf能够将一个对象以特定的格式转换为一个二进制串(序列化),然后将二进制串还原成对象(反序列化)。这里涉及到两个指标:
对同一个目标对象:
1)序列化和反序列化的时间开销,
2)序列化之后串的长度
protobuf在这两个方面都有非常出色的表现(网传)
在Windows下使用protobuf的步骤如下:
第一步:
下载protoc-2.5.0-win32.zip,得到其中的protoc.exe.然后将该protoc.exe的存放路径加入Path环境变量,便于访问。比如,我的protoc.exe存放于D:/protobuf,环境变量中添加如下配置:
D:/protobuf
第二步:
编写.proto文件,它是序列化一个对象的“模板”,protobuf就是根据它来决定如何序列化和反序列化。
编写的person-entity.proto配置文件如下:
option java_outer_classname = "PersonEntity";//生成的数据访问类的类名 message Person { required int32 id = 1;//同上 required string name = 2;//必须字段,在后面的使用中必须为该段设置值 optional string email = 3;//可选字段,在后面的使用中可以自由决定是否为该字段设置值 }
message字段代表了一个对象,所以,可以使用message实现对象的嵌套序列化
required表示是强制字段,在后面的使用中必须为该字段设置值;
optional表示是可选字段,在后面的使用中可选地为该字段设置值;
repeated表示集合类型,可以填充多个数据
后面的1,2,3是字段的编号,字段名是让用户使用的,字段编号则是让系统识别的,从1开始。
protobuf自定义的数据类型和Java的数据类型的对应关系见这篇博客https://blog.csdn.net/chuhui1765/article/details/100670318
可以指定字段类型为其他的Message字段类型:
message SearchResponse{ repeated Result result = 1; } message Result{ required string url = 1; optional string title = 2; repeated string snippets = 3; }
可以在message内部嵌套另一个message:
message SearchResponse{ message Result{ required string url = 1; optional string title = 2; repeated string snippets = 3; } repeated Result result = 1; }
有关protobuf更详细的用法,可以参考这篇优秀的博文https://worktile.com/tech/share/prototol-buffers
第三步:
用protoc.exe生成PersonEntity.class。打开DOS窗口,输入如下的编译命令:
protoc.exe -I=D:/protobuf --java_out=D:/protobuf D:/protobuf/person-entity.proto
编译命令的格式如下:
protoc.exe -I=.protoc文件的存放路径 --java_out=.class文件的输出路径 .protoc文件的具体存放路径
其中第三个路径是必须的,而且要写明.protoc文件
最后会生成PersonEntity.java文件,可以把它理解为一个工具类,帮助我们执行对象的序列化。
第四步:
新建maven项目,将PersonEntity.java复制到项目中,引入maven依赖
<dependency> <groupId>com.google.protobuf</groupId> <artifactId>protobuf-java</artifactId> <version>2.5.0</version> </dependency>
利用工具类序列化和反序列化对象。
/* author:chxy data:2020/4/1 description: */ import java.io.IOException; public class Test { public static void main(String[] args) throws IOException { //模拟将对象转成byte[],方便传输 PersonEntity.Person.Builder builder = PersonEntity.Person.newBuilder(); builder.setId(1); builder.setName("abc"); builder.setEmail("def"); PersonEntity.Person person = builder.build(); System.out.println("after :" +person.toString()); final byte[] bytes = person.toByteArray(); System.out.println("===========Person Byte=========="); for(byte b : person.toByteArray()){ System.out.print(b); } System.out.println(); //反序列化成Person类 byte[] byteArray =person.toByteArray(); PersonEntity.Person person2 = PersonEntity.Person.parseFrom(byteArray); System.out.println("after :" +person2.toString()); } }
这里还犯过一个错:另外自定义一个Person类与PersonEntity.Person相对应。实际上在我们的项目中可以直接利用这个PersonEntity.Person来实现功能需求。
下面同样以这个Person类序列化为例,来实际地比较通过各种不同的序列化方式得到的的字节数组长度:
1.protobuf
序列化长度为:
System.out.println(person.getSerializedSize());
结果为:12
2.Java原生序列化
/* author:chxy data:2020/4/2 description: */ import java.io.*; public class Test2 { public static void main(String[] args) throws IOException { Person p = new Person(1,"abc","def"); ObjectOutputStream objectOutputStream = new ObjectOutputStream( new FileOutputStream("E:/object.txt")); objectOutputStream.writeObject(p); FileInputStream fileInputStream = new FileInputStream("E:/object.txt"); final int length = fileInputStream.read(); System.out.println(length); } }
结果为:172
3.通过对象的toString()
序列化:重写对象的toString()方法,按照特定的格式,将对象的各个字段封装进一个String字符串,再将字符串按照特定的编码方式,转换成byte数组,实现本地存储和网络传输。
反序列化:对byte数组,重新转换成字符串,再将字符串按照约定的格式,还原成对象的属性,从而构造出原来的对象。
这里重点探讨的是:String对象转换成byte数组之后的长度
public static void main(String[] args) { String s = "abc"; System.out.println(s.getBytes().length); String s2 = "嘤嘤嘤"; System.out.println(s2.getBytes().length);
System.out.println(Charset.defaultCharset());
}
输出结果为:
3
9
UTF-8
可见,如果是英文字母每个字符占一个字节,如果是汉字,每个字符占3个字节。如果通过这种方式序列化,它的空间效率也是非常高的。
这里有必要搞清楚Java默认的字符编码集:
/** * Encodes this {@code String} into a sequence of bytes using the * platform's default charset, storing the result into a new byte array. * * <p> The behavior of this method when this string cannot be encoded in * the default charset is unspecified. The {@link * java.nio.charset.CharsetEncoder} class should be used when more control * over the encoding process is required. * * @return The resultant byte array * * @since JDK1.1 */ public byte[] getBytes() { return StringCoding.encode(value, 0, value.length); }
Java采用平台的默认字符编码集来进行字符与二进制byte序列的转换。当然也可以在序列化的时候显示指定字符编码集。
Unicode 与 UTF-8
为什么会出现unicode,这首先要从ASCII码说起,它起源最早,用7位来表示英文字母、数字等字符。但是后来又出现了很多其他语言的字符,比如汉字,ASCII码最多只能表示256个字符,无法支持这些”异域“字符,因此出现了unicode码,将各种语言的字符用一种统一的编码方式转换成01序列。但是新的问题又出现了,unicode编码效率不高,原来一个字符a用ASCII表示只需要一个字符,现在用unicode可能用3个字符,存储效率和网络传输效率大打折扣。因此出现了中间转换编码集,utf-8就是其中之一。”UTF-8(8-bit Unicode Transformation Format)是一种针对Unicode的可变长度字符编码,又称万国码“,注意,它的编码长度是可变的。所以,unicode是utf-8的基础,utf-8是对unicode的一种解释。