ProtoBuf 序列化

957 阅读6分钟

一、概述

1、前言

  • 常见的序列化协议:
    • JSON
    • XML
    • Hessian
    • Thrift
  • ProtoBuf 的优点:
    • Protobuf解析速度快(即序列化反序列化速度快),

    • 占用空间小,以及兼容性好,很适合做数据存储或网络通讯间的数据传输。

2、JDK原生序列化

package protobuf;

public class Teacher implements Serializable {

    private long teacherId;
    private String name;
    private int age;
    private List<String> courses = new ArrayList<>();

    public Teacher(long teacherId, String name, int age) {
        this.teacherId = teacherId;
        this.name = name;
        this.age = age;
    }

    // getter and setter...
    @Override
    public String toString() {
        return "Teacher{" +
                "teacherId=" + teacherId +
                ", name='" + name + ''' +
                ", age=" + age +
                ", courses=" + courses +
                '}';
    }
}
public class Test_JDK {
    public static void main(String[] args) throws Exception {
        Teacher tim = new Teacher(1L, "Tim", 34);
        tim.setCourses(new ArrayList<>(Arrays.asList("aaaa", "aaaa")));
        // 序列化
        byte[] byteArray = serialize(tim);
        System.out.println(Arrays.toString(byteArray));
        // 反序列化
        Teacher teacher = deserialize(byteArray);
        System.out.println(teacher);
    }

    // 序列化
    private static byte[] serialize(Teacher tim) throws IOException {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = new ObjectOutputStream(bos);
        oos.writeObject(tim);
        return bos.toByteArray();
    }

    // 反序列化
    private static Teacher deserialize(byte[] bytes) throws Exception {
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(bytes));
        return (Teacher) ois.readObject();
    }


}
  • 输出
[-84, -19, 0, 5, 115, 114, 0, 16, 112, 114, 111, 116, 111, 98, 117, 102, 46, 84, 101, 97, 99, 
104, 101, 114, -109, 117, -76, 44, 106, 50, -50, -61, 2, 0, 4, 73, 0, 3, 97, 103, 101, 74, 0, 
9, 116, 101, 97, 99, 104, 101, 114, 73, 100, 76, 0, 7, 99, 111, 117, 114, 115, 101, 115, 116, 
0, 16, 76, 106, 97, 118, 97, 47, 117, 116, 105, 108, 47, 76, 105, 115, 116, 59, 76, 0, 4, 110, 
97, 109, 101, 116, 0, 18, 76, 106, 97, 118, 97, 47, 108, 97, 110, 103, 47, 83, 116, 114, 105, 
110, 103, 59, 120, 112, 0, 0, 0, 34, 0, 0, 0, 0, 0, 0, 0, 1, 115, 114, 0, 19, 106, 97, 118, 97, 
46, 117, 116, 105, 108, 46, 65, 114, 114, 97, 121, 76, 105, 115, 116, 120, -127, -46, 29, -103, 
-57, 97, -99, 3, 0, 1, 73, 0, 4, 115, 105, 122, 101, 120, 112, 0, 0, 0, 2, 119, 4, 0, 0, 0, 2, 
116, 0, 4, 97, 97, 97, 97, 113, 0, 126, 0, 6, 120, 116, 0, 3, 84, 105, 109]

Teacher{teacherId=1, name='Tim', age=34, courses=[aaaa, aaaa]}

3、通过Protobuf 序列化

  • 首先下载:
https://github.com/protocolbuffers/protobuf/releases/download/v3.7.0/protobuf-java-3.7.0.zip
https://github.com/protocolbuffers/protobuf/releases/download/v3.7.0/protoc-3.7.0-win64.zip
  • 需要定义一个teacher.proto
syntax = "proto2";
option java_package = "edu.xpu";
option java_outer_classname = "TeacherSerializer";
message Teacher{
	required int64 teacherId = 1;
	required int32 age = 2;
	required string name = 3;
	repeated string courses = 4;
}

message xxx {
  // 字段规则:required -> 字段只能也必须出现 1 次
  // 字段规则:optional -> 字段可出现 0 次或1次
  // 字段规则:repeated -> 字段可出现任意多次(包括 0)
  // 类型:int32、int64、sint32、sint64、string、32-bit ....
  // 字段编号:0 ~ 536870911(除去 19000 到 19999 之间的数字)
  字段规则 类型 名称 = 字段编号;
}

image.png

  • 当前生成的Java文件中用到的类还需要我们引入Protobuf的依赖:
<dependency>
    <groupId>com.google.protobuf</groupId>
    <artifactId>protobuf-java</artifactId>
    <version>3.13.0</version>
</dependency>
  • 把生成的Java文件给复制到工程当中,然后测试一下序列化和反序列化
import java.util.Arrays;
public class ProtobufTest {
    public static void main(String[] args) throws Exception {
        byte[] bytes = serialize();
        System.out.println(Arrays.toString(bytes));

        TeacherSerializer.Teacher teacher = deserialize(bytes);
        System.out.println(teacher);
    }
    // 序列化
    private static byte[] serialize(){
        // 构造器,构造Teacher
        TeacherSerializer.Teacher.Builder builder = TeacherSerializer.Teacher.newBuilder();
        builder.setName("Tim")
                .setAge(34)
                .setTeacherId(1L)
                .addCourses("Java");
        TeacherSerializer.Teacher teacher = builder.build();
        return teacher.toByteArray();
    }
    // 反序列化
    private static TeacherSerializer.Teacher deserialize(byte[] bytes) throws Exception {
        return TeacherSerializer.Teacher.parseFrom(bytes);
    }
}
  • 同样属性的JavaBean对象,但是通过Protobuf序列化和反序列化的代价却小很多,和Java原生序列化的大小产生了鲜明对比:

image.png

二、Protobuf特性与基本原理

1、将字段长度减小

  • 对于一条信息,json的表示方式为:
{ "age": 30, "name": "zhangsan",  "height": 175.33, "weight": 140 }
  • 存在大量的冗余数据
  • 如果这样子存储,就可以减少大量的数据

image.png

2、进一步压缩

  • 假设height这个字段为null,我们其实是不必要传递这个字段的,这个时候我们需要传递的数据就为:

image.png

  • tag 技术 image.png

  • 每个字段我们都用tag|value的方式来存储的,在tag当中记录两种信息,一个是value对应的字段的编号,另一个是value的数据类型(比如是整形还是字符串等),因为tag中有字段编号信息,所以即使没有传递height字段的value值,根据编号也能正确的配对。

  • 传统的 json 序列化中, tag 存储的是 字符串

  • protobuf 序列化, tag 存储的是二进制编号,一般只会占据一个字节

3、protobuf支持的字段类型

image.png

因为tag一般占用一个字节,开销还算是比较小的,所以protobuf整体的存储空间占用还是相对小了很多的。

4、进一步压缩

  • 在实际的传输过程中,会传递整数,我们知道整数在计算机当中占据4个字节,但是绝大部分的整数,比如价格,库存等,都是比较小的整数,实际用不了4个字节,像127这种数,在计算机中的二进制是:00000000 00000000 00000000 01111111(4字节32位)

  • 完全可以用最后1个字节来进行存储,protobuf当中定义了Varint这种数据类型,可以以不同的长度来存储整数,将数据进一步的进行了压缩。

  • 但是这里面也有一个问题,在计算机当中的负数是用补码表示的,对于-1,它的二进制表示方式为:11111111 11111111 11111111 11111111(4字节32位)显然无法用1个字节来表示了,

  • 但-1确实是一个比较简单的数,这个时候就可以使用算法来对负数进行进一步的压缩,最终我们可以使用2个字节来表示-1。

5、 要快

  • 虽然数据现在很小了,但是解析速度还是有很大的提升空间的,因为每个字段都是用tag|value来表示的,在tag中含有value的数据类型的信息,而不同的数据类型有不同的大小,比如如果valuebool型,我们就知道肯定占了一个字节,程序从tag后面直接读一个字节就可以解析出value,非常快,而json则需要进行字符串解析才可以办到。

参考