基于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.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中应用这些编解码框架。