基于Java提供的对象输入/输出流ObjectlnputStream和ObjectOutputStream,可以直接把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.010 */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 userName34 */35 public final String getUserName() {36 return userName;37 }38 39 /**40 * @param userName41 * the userName to set42 */43 public final void setUserName(String userName) {44 this.userName = userName;45 }46 47 /**48 * @return the userID49 */50 public final int getUserID() {51 return userID;52 }53 /**54 * @param userID55 * the userID to set56 */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序列化代码 编码测试类TestUserlnfopackage 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 Administrator10 * @date 2014年2月23日11 * @version 1.012 */13 public class PerformTestUserInfo {14 15 /**16 * @param args17 * @throws IOException18 */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中应用这些编解码框架。