Featured image of post RPC常用的序列化

RPC常用的序列化

JDK原生序列化

如果使用Java语言开发,JDK原生就有序列化:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;

public class Student implements Serializable {
    //学号
    private int no;
    //姓名
    private String name;

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        String home = System.getProperty("user.home");
        String basePath = home + "/Desktop";
        FileOutputStream fos = new FileOutputStream(basePath + "student.dat");
        Student student = new Student();
        student.setNo(100);
        student.setName("TEST_STUDENT");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(student);
        oos.flush();
        oos.close();
        FileInputStream fis = new FileInputStream(basePath + "student.dat");
        ObjectInputStream ois = new ObjectInputStream(fis);
        Student deStudent = (Student) ois.readObject();
        ois.close();
        System.out.println(deStudent);
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

JDK自带的序列化机制很简单。序列化具体的实现是有ObjectOutputStream完成的,而反序列化的具体实现是由ObjectInputStream完成的。序列化的过程就是在读取对象数据的时候,不断加入一些特殊的分隔符,这些分隔符用于在反序列化的过程中截断使用:

http://image.maishuren.top/rpc/jdk-serialization.jpg-msr

头部数据:声明序列化协议、序列化版本、用于高低版本向后兼容

对象数据:主要包含类名、签名、属性、属性类型以及属性值,还有开头结尾分隔符等数据。除了属性值属于真正的对象值,其他都是用于反序列化用的元数据。

当存在对象引用、继承的情况下,就是递归遍历写对象的逻辑。

反序列化框架的核心思想就是设计一种序列化协议。将数据按照固定格式写到二进制字节流来完成序列化,在按照固定的格式反序列化成对象,通过这些信息重新创建一个新的对象,来完成反序列化。

JSON

JSON可以说是我们最熟悉的了,它没有数据类型,是典型的Key-Value结构形式。一般基于HTTP协议的RPC框架,都会选择JSON格式来传输数据。

但是使用JSON进行序列化有两个问题:

1、JSON进行序列化的额外空间开销比较大,对于大数据量服务就意味着需要巨大的内存和磁盘开销。

2、JSON没有类型,但像Java这种强类型与语言,需要通过反射统一解决,所以性能不会太号。

如果RPC框架使用JSON进行序列化,服务调用传输的数据量要相对较小,不然会影响性能。

Hessian

Hessian是动态类型、二进制、紧凑的,并且跨语言一直的一种序列化框架。Hessian协议要比JDK、JSON更加紧凑,性能要比JDK、JSON序列化高效很多,而且生成的字节数更小。这样进行网络传输的时候更加高效,性能更好。

Hessian是第三方包,在使用的时候需要引入到工程里面使用:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
public class Student implements Serializable {
    //学号
    private int no;
    //姓名
    private String name;

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        Student student = new Student();
        student.setNo(101);
        student.setName("HESSIAN");
		// 把student对象转化为byte数组
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Hessian2Output output = new Hessian2Output(bos);
        output.writeObject(student);
        output.flushBuffer();
        byte[] data = bos.toByteArray();
        bos.close();
        // 把刚才序列化出来的byte数组转化为student对象
        ByteArrayInputStream bis = new ByteArrayInputStream(data);
        Hessian2Input input = new Hessian2Input(bis);
        Student deStudent = (Student) input.readObject();
        input.close();
        System.out.println(deStudent);
    }

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}

Hessian比JDK远程序列化、JSON更加高效、生成的字节数更小,有非常好兼容性和稳定性,所以Hessian更加适合作为RPC框架的序列化协议。

Hessian的问题,官方版本对Java里面一些常见的类型不支持,比如:

1、Linked系列,LinkedHashMap、LinkedHashSet等,需要通过扩展CollectionDeserializer类来修复。

2、Local类,可以通过扩展ContextSerializerFactory类来修复。

3、Byte/Short反序列化的之后会变成Integer类型。

这些问题在实际使用时需要注意。

Protobuf

Protobuf是Google内部混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化。它支持很多中语言,如Java、Python、C++、Go等。Protobuf使用的时候需要定义IDL,然后使用不同语言的IDL(Intertace description language)编译器,生成序列化工具类,有点是:

1、序列化后体积相比JSON、Hessian小很多

2、IDL可以清晰地描述予以,可以帮助并保证应用程序之间地类型不会丢失,无需类似XML解析器

3、序列化和反序列化速度很快,不需要通过反射获取数据类型

4、消息格式升级和兼容性不错,可以做到向后兼容

要使用首先是需要编写一个protobuf文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
 // IDl 文件格式
 synax = "proto3";
 option java_package = "com.test";
 option java_outer_classname = "StudentProtobuf";

 message StudentMsg {
  //序号
  int32 no = 1;
  //姓名
  string name = 2;
 }
}

通过protobuf的工具将proto文件生成工具类

1
2
3
4
5
6
7
8
9
StudentProtobuf.StudentMsg.Builder builder = StudentProtobuf.StudentMsg.newBuil
builder.setNo(103);
builder.setName("protobuf");
// 把student对象转化为byte数组
StudentProtobuf.StudentMsg msg = builder.build();
byte[] data = msg.toByteArray();
// 把刚才序列化出来的byte数组转化为student对象
StudentProtobuf.StudentMsg deStudent = StudentProtobuf.StudentMsg.parseFrom(dat
System.out.println(deStudent);

Protobuf 非常高效,但是对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如Hessian,比如用 Java 的话,这个预编译过程不是必须的,可以考虑使用Protostuff。

Protostuff 不需要依赖 IDL 文件,可以直接对 Java 领域对象进行反 / 序列化操作,在效率上跟Protobuf 差不多,生成的二进制格式和 Protobuf 是完全相同的,可以说是一个 Java版本的 Protobuf序列化框架。但是ProtoStuff不支持单纯的MAp、List集合对象,需要包在对象里面。

How to choose

1、性能和效率

这是一个非常值得考虑的因素,很多系统要追求高性能,序列化和反序列化是调用RPC必须的一个过程,那么序列化和反序列化的性能和效率会直接影响RPC框架整体的性能和效率。

2、空间开销

上面列出来的序列化类型中,都有比较序列化后的二级制数据大小。序列化后的字节数据体积越小,在网络传输中的数据量就越小,传输数据的速度也就越快,由于RPC是远程调用,需要通过网络传输,网络传输的速度将直接影响到请求响应的耗时。

3、通用性、兼容性

序列化协议的通用性和兼容性问题可以说是最常见的问题了。如果是支持跨编程语言,那么不同编程语言的数据类型是否都能兼容支持。在序列化的选择上,效率、性能、序列化协议后的体积相比,通用性和兼容性可以说是优先级更加的高。因为这会直接影响服务调用的稳定性和可用性,应该让RPC框架更加的可靠。

除了序列化协议的通用性和兼容性,序列化协议的安全性也是非常重要的一个参考因素,甚至应该放在第一位去考虑。以 JDK 原生序列化为例,它就存在漏洞。如果序列化存在安全漏洞,那么线上的服务就很可能被入侵。

在选在序列化的考虑可以这样选择:安全性 → 通用性 → 兼容性 → 性能 → 效率 → 空间开销

如果让我来选的话会在Hessian和Protobuf之间选择,两者在性能、时间开销、空间开销、通用性、兼容性和安全性,都让人满足。Hessian使用更加方便,兼容性更好,不需要额外编写proto文件。Protobuf的话则是更加高效和通用。

使用RPC时序列化应该关注的问题

不要使用复杂对象作为参数:

如果一个对象里面嵌套关联另外的对象,这个对象又关联其他对象,如此套娃的话。在序列化的时候,会很浪费性能,消耗CPU,从而影响RPC框架的性能。

传参不要传大对象:

如果入参对象非常大,比如大List或大Map,序列化之后的体积还很大,这样也会浪费性能和CPU,并且序列化如此大的一个对象时很耗费时间的,这也会影响RPC的性能。

不要使用支持的类作为入参:

有时候项目会引入一些第三方库,这些库有自己的一些集合类型,比如Guava中的集合类。很多的序列化的框架都是优先支持编程语言的原生类型。所以入参集合类型,尽量使用常用的原生集合类。

入参不要有复杂的继承关系:

大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性。就像对象嵌套一样,对象关系越复杂,就越浪费性能,同时又很容易出现序列化上的问题。

Licensed under CC BY-NC-SA 4.0