(中级篇 NettyNIO编解码开发)第六章-编解码技术

基于Java提供的对象输入/输出流ObjectlnputStreamObjectOutputStream,可以直接把Java对象作为可存储的字节数组写入文件,也可以传输到网络上。对程序员来说,基于JDK默认的序列化机制可以避免操作底层的字节数组,从而提升开发效率。Java序列化的目的主要有两个:

1.网络传输
2.对象持久化

由于本书主要介绍基于Netty的NIO网络开发,所以我们重点关注网络传输。当选行远程跨迸程服务调用时,需要把被传输的Java对象编码为字节数组或者ByteBuffer对象。而当远程服务读取到ByteBuffer对象或者字节数组时,需要将其解码为发送时的Java 对象。这被称为Java对象编解码技术。


Java序列化仅仅是Java编解码技术的一种,由于它的种种缺陷,衍生出了多种编解码技术和框架,后续的章节我们会结合Netty介绍几种业界主流的编解码技术和框架,看看如何在Netty中应用这些编解码框架实现消息的高效序列化。
本章主要内容包括:

1.Java序列化的缺点

2.业界流行的几种编解码框架介绍


 

6.1    Java序列化的缺点

Java序列化从JDK1.1版本就已经提供,它不需要添加额外的类库,只需实现java.io.Serializable并生成序列ID即可,因此,它从诞生之初就得到了广泛的应用。
但是在远程服务调用(RPC)时,很少直接使用Java序列化进行消息的编解码和传输,这又是什么原因呢?下面通过分析.Tava序列化的缺点来找出答案。

6.1.1    无法跨语言

无法跨语言,是Java序列化最致命的问题。对于跨进程的服务调用,服务提供者可能会使用C十+或者其他语言开发,当我们需要和异构语言进程交互时Java序列化就难以胜任。

由于Java序列化技术是Java语言内部的私有协议,其他语言并不支持,对于用户来说它完全是黑盒。对于Java序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。
事实上,目前几乎所有流行的JavaRCP通信框架,都没有使用Java序列化作为编解码框架,原肉就在于它无法跨语言,而这些RPC框架往往需要支持跨语言调用。

6.1.2    序列化后的码流太大
下面我们通过一个实例看下Java序列化后的字节数组大小。

Java序列化代码  POJO对象类    UserInfo

 1 package lqy5_serializable_115;
 2 
 3 import java.io.Serializable;
 4 import java.nio.ByteBuffer;
 5 
 6 /**
 7  * @author Administrator
 8  * @date 2014年2月23日
 9  * @version 1.0
10  */
11 public class UserInfo implements Serializable {
12 
13     /**
14      * 默认的序列号
15      */
16     private static final long serialVersionUID = 1L;
17 
18     private String userName;
19 
20     private int userID;
21 
22     public UserInfo buildUserName(String userName) {
23     this.userName = userName;
24     return this;
25     }
26 
27     public UserInfo buildUserID(int userID) {
28     this.userID = userID;
29     return this;
30     }
31 
32     /**
33      * @return the userName
34      */
35     public final String getUserName() {
36     return userName;
37     }
38 
39     /**
40      * @param userName
41      *            the userName to set
42      */
43     public final void setUserName(String userName) {
44     this.userName = userName;
45     }
46 
47     /**
48      * @return the userID
49      */
50     public final int getUserID() {
51     return userID;
52     }
53     /**
54      * @param userID
55      *            the userID to set
56      */
57     public final void setUserID(int userID) {
58     this.userID = userID;
59     }
60 
61     public byte[] codeC() {
62     ByteBuffer buffer = ByteBuffer.allocate(1024);
63     byte[] value = this.userName.getBytes();
64     buffer.putInt(value.length);
65     buffer.put(value);
66     buffer.putInt(this.userID);
67     buffer.flip();
68     value = null;
69     byte[] result = new byte[buffer.remaining()];
70     buffer.get(result);
71     return result;
72     }
73 
74     public byte[] codeC(ByteBuffer buffer) {
75     buffer.clear();
76     byte[] value = this.userName.getBytes();
77     buffer.putInt(value.length);
78     buffer.put(value);
79     buffer.putInt(this.userID);
80     buffer.flip();
81     value = null;
82     byte[] result = new byte[buffer.remaining()];
83     buffer.get(result);
84     return result;
85     }
86 }

Userlnfo对象是个普通的POJO对象,它实现了java.io.SerializabIe接口,并且生成了一个默认的序列号serialVersionUID=lL,这说明UserInfo对象可以通过JDK默认的序列化机制进行序列化和反序列化。
第61~72行使用基于ByteBuffer的通用二进制编解码技术对UserInfo对象进行编码,编码结果仍然是byte数组,可以与传统的JDK序列化后的码流大小进行对比。


下面写一个测试程序,先调用两种编码接口对POJO对象编码,然后分别打印两者编码后的码流大小进行对比。


Java序列化代码    编码测试类TestUserlnfo

package lqy5_serializable_115;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;

/**
 * @author Administrator
 * @date 2014年2月23日
 * @version 1.0
 */
public class TestUserInfo {

    /**
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
    UserInfo info = new UserInfo();
    info.buildUserID(100).buildUserName("Welcome to Netty");
    ByteArrayOutputStream bos = new ByteArrayOutputStream();
    ObjectOutputStream os = new ObjectOutputStream(bos);
    os.writeObject(info);
    os.flush();
    os.close();
    byte[] b = bos.toByteArray();
    System.out.println("The jdk serializable length is : " + b.length);
    bos.close();
    System.out.println("-------------------------------------");
    System.out.println("The byte array serializable length is : "
        + info.codeC().length);

    }

}

结果是

 

测试结果令人震惊,采用JDK    序列化机制编码后的二迸制数组大小竟然是二进制编码的5.29倍。
我们评判一个编解码框架的优劣时,往往会考虑以下几个因素。


1.是否支持跨语言,支持的语言种类是否丰富;

2.编码后的码流大小:

3.编解码的性能;

4.类库是否小巧,API使用是否方便:

5.使用者需要手工开发的工作量和难度。



在同等情况下,编码后的字节数组越大,存储的时候就越占空间,存储的硬件成本就
越高,并且在网络传输时更占带宽,导致系统的吞吐量降低。Java序列化后的码流偏大也一直被业界所垢病,导致它的应用范围受到了很大限制。

6.1.3    序列化性能太低

下面我们从序列化的性能角度看下JDK    的表现如何。

创建一个性能测试版本 的 PerformTestUserInfo测试程序 ,代码如下 。

 

 1 package lqy5_serializable_115;
 2 
 3 import java.io.ByteArrayOutputStream;
 4 import java.io.IOException;
 5 import java.io.ObjectOutputStream;
 6 import java.nio.ByteBuffer;
 7 
 8 /**
 9  * @author Administrator
10  * @date 2014年2月23日
11  * @version 1.0
12  */
13 public class PerformTestUserInfo {
14 
15     /**
16      * @param args
17      * @throws IOException
18      */
19     public static void main(String[] args) throws IOException {
20     UserInfo info = new UserInfo();
21     info.buildUserID(100).buildUserName("Welcome to Netty");
22     int loop = 1000000;
23     ByteArrayOutputStream bos = null;
24     ObjectOutputStream os = null;
25     long startTime = System.currentTimeMillis();
26     for (int i = 0; i < loop; i++) {
27         bos = new ByteArrayOutputStream();
28         os = new ObjectOutputStream(bos);
29         os.writeObject(info);
30         os.flush();
31         os.close();
32         byte[] b = bos.toByteArray();
33         bos.close();
34     }
35     long endTime = System.currentTimeMillis();
36     System.out.println("The jdk serializable cost time is  : "
37         + (endTime - startTime) + " ms");
38 
39     System.out.println("-------------------------------------");
40 
41     ByteBuffer buffer = ByteBuffer.allocate(1024);
42     startTime = System.currentTimeMillis();
43     for (int i = 0; i < loop; i++) {
44         byte[] b = info.codeC(buffer);
45     }
46     endTime = System.currentTimeMillis();
47     System.out.println("The byte array serializable cost time is : "
48         + (endTime - startTime) + " ms");
49 
50     }
51 
52 }

对Java序列化和二迸制编码分别进行性能测试,编码100万次,然后统计耗费的总时间,测试结果如图

 

 

从图6-4可以看出,无论是序列化后的码流大小,还是序列化的性能,JDK默认的序列化机制表现得都很差。因此,我们边常不会选择Java序列化作为远程跨节点调用的编解码框架。
但是不使用JDK提供的默认序列化框架,自己开发编解码框架又是个非常复杂的工作,怎么办呢?不用着急,业界有很多优秀的编解码框架,它们在克服了JDK默认序列化框架缺点的基础上,还增加了很多亮点,下面让我们继续了解并学习业界流行的几款编解码框架。


6.2    业界主流的编解码框架

由于Java的编解码框架五花八门,穷举学习显然不是一个好的策略,本节挑选了一些业界主流的编解码框架和编解码技术进行介绍,希望读者在了解这些框架特性的基础上,做出合理的选择。

6.2.1    Google的Protobuf介绍

Protobuf全称GoogleProtocolBuffers,它由谷歌开源而来,在谷歌内部久经考验。它将数据结构以.proto文件进行描述,通过代码生成工具可以生成对应数据结构的POJO对象和Protobuf相关的方法和属性。
它的特点如下。

1.结构化数据存储格式(XML,JSON等〉:
2.高效的编解码性能:
3.语言无关、平台无关、扩展性好;

4.官方支持Java、C++和Python三种语言。

首先我们来看下为什么不使用XML,尽管XML的可读性和可扩展性非常好?也非常适合描述数据结构,但是XML解析的时间开销和XML为了可读性而牺牲的空间开销都非常大,因此不适合做高性能的通信协议。Protobuf使用二进制编码,在空间和性能上具有更大的优势。

Protobut另一个比较吸引人的地方就是它的数据描述文件和代码生成机制,利用数据描述文件对数据结构进行说明的优点如下。
1.文本化的数据结构描述语言,可以实现语言和平台尤关,特别适合异构系统间的集成:
2.通过标识字段的顺序,可以实现协议的前向兼容:
3.自J代码生成,不需要手工编写同样数据结构的C++和Java版本;
4.方便后续的管理和维护。相比于代码,结构化的文档更容易管理和维护。

下面我们看下Protobuf    编解码和其他几种序列化框架的性能对比数据,如图

 

 

从图可以发现,Protobuf 的编解码性能远远离于其他几种序列化框架的序列化和反序列化,这也是很多RPC框架选用Protobuf做编解码框架的原因。


 

6.2.2    Facebook的Thrift介绍



6.2.3    JBossMarshalling介绍



6.3    总结

首先对Java的序列化技术进行了介绍,对Java序列化的缺点进行了总结说明,在此基础上引出了几款业界主流的编解码框架。由于编解码框架种类繁多,无法一一枚举,所以重点介绍了当前最流行的几种编解码框架。后续在第7章我们会对这些编解码框架的使用进行说明,并给出具体的示例,同时,讲解如何在Netty中应用这些编解码框架。

 

posted @ 2015-10-22 15:14  crazyYong  阅读(1465)  评论(0编辑  收藏  举报