Netty编解码开发指南

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

     1.网络传输

     2.对象持久化

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

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

本章主要内容包括:

   1.Java序列化的缺点

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

   3.性能测试的对比

1.1    Java序列化的缺点

       Java序列化从JDK1.1版本就已经提供,它不需要添加额外的类库,只需实现java.io.Serializable并生成序列ID即可,因此,它从诞生之初就得到了广泛的应用。

      但是在远程服务调用(RPC)时,很少直接使用Java序列化进行消息的编解码和传输,这又是什么原因呢?下面通过分析.Tava序列化的缺点来找出答案。

1.1.1    无法跨语言

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

      由于Java序列化技术是Java语言内部的私有协议,其他语言并不支持,对于用户来说它完全是黑盒。对于Java序列化后的字节数组,别的语言无法进行反序列化,这就严重阻碍了它的应用。

事实上,目前几乎所有流行的JavaRCP通信框架,都没有使用Java序列化作为编解码框架,原肉就在于它无法跨语言,而这些RPC框架往往需要支持跨语言调用。

1.1.2    序列化后的码流太大

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

  Java序列化代码  POJO对象类    UserInfo

public class UserInfo implements Serializable {

	/**
	 * 默认的序列号
	 */
	private static final long serialVersionUID = 1L;

	private String userName;

	private int userID;

	public UserInfo buildUserName(String userName) {
		this.userName = userName;
		return this;
	}

	public UserInfo buildUserID(int userID) {
		this.userID = userID;
		return this;
	}

	/**
	 * @return the userName
	 */
	public final String getUserName() {
		return userName;
	}

	/**
	 * @param userName
	 *            the userName to set
	 */
	public final void setUserName(String userName) {
		this.userName = userName;
	}

	/**
	 * @return the userID
	 */
	public final int getUserID() {
		return userID;
	}

	/**
	 * @param userID
	 *            the userID to set
	 */
	public final void setUserID(int userID) {
		this.userID = userID;
	}

	public byte[] codeC() {
		ByteBuffer buffer = ByteBuffer.allocate(1024);
		byte[] value = this.userName.getBytes();
		buffer.putInt(value.length);
		buffer.put(value);
		buffer.putInt(this.userID);
		buffer.flip();
		value = null;
		byte[] result = new byte[buffer.remaining()];
		buffer.get(result);
		return result;
	}

	public byte[] codeC(ByteBuffer buffer) {
		buffer.clear();
		byte[] value = this.userName.getBytes();
		buffer.putInt(value.length);
		buffer.put(value);
		buffer.putInt(this.userID);
		buffer.flip();
		value = null;
		byte[] result = new byte[buffer.remaining()];
		buffer.get(result);
		return result;
	}
}


public class TestUserInfo {

	public static void main(String[] args) throws IOException {
		UserInfo info = new UserInfo();
		info.buildUserID(100).buildUserName("Welcome to Netty");

		ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
		ObjectOutputStream objectOutputStream = new ObjectOutputStream(
				outputStream);

		objectOutputStream.writeObject(info);
		objectOutputStream.flush();
		objectOutputStream.close();

		byte[] bytes = outputStream.toByteArray();
		System.out.println("The jdk serializable length is : " + bytes.length);
		outputStream.close();

		System.out.println("-------------------------------------");
		System.out.println("The byte array serializable length is : "
				+ info.codeC().length);
	}
}

输出结果:

The jdk serializable length is : 115
-------------------------------------
The byte array serializable length is : 24

 测试结果令人震惊,采用JDK    序列化机制编码后的二迸制数组大小竟然是二进制编码的5.29倍。

 我们评判一个编解码框架的优劣时,往往会考虑以下几个因素。

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

2.编码后的码流大小:

3.编解码的性能;

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

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

   在同等情况下,编码后的字节数组越大,存储的时候就越占空间,存储的硬件成本就

越高,并且在网络传输时更占带宽,导致系统的吞吐量降低。Java序列化后的码流偏大也一直被业界所垢病,导致它的应用范围受到了很大限制。

1.1.3    序列化性能太低

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

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

public class PerformTestUserInfo {

	/**
	 * @param args
	 * @throws IOException
	 */
	public static void main(String[] args) throws IOException {
		UserInfo info = new UserInfo();
		info.buildUserID(100).buildUserName("Welcome to Netty");
		int loop = 1000000;
		ByteArrayOutputStream bos = null;
		ObjectOutputStream os = null;
		long startTime = System.currentTimeMillis();
		for (int i = 0; i < loop; i++) {
			bos = new ByteArrayOutputStream();
			os = new ObjectOutputStream(bos);
			os.writeObject(info);
			os.flush();
			os.close();
			byte[] b = bos.toByteArray();
			bos.close();
		}
		long endTime = System.currentTimeMillis();
		System.out.println("The jdk serializable cost time is  : "
				+ (endTime - startTime) + " ms");

		System.out.println("-------------------------------------");

		ByteBuffer buffer = ByteBuffer.allocate(1024);
		startTime = System.currentTimeMillis();
		for (int i = 0; i < loop; i++) {
			byte[] b = info.codeC(buffer);
		}
		endTime = System.currentTimeMillis();
		System.out.println("The byte array serializable cost time is : "
				+ (endTime - startTime) + " ms");
	}
}

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

   image.png

   image.png

   从图可以看出,无论是序列化后的码流大小,还是序列化的性能,JDK默认的序列化机制表现得都很差。因此,我们边常不会选择Java序列化作为远程跨节点调用的编解码框架。

     但是不使用JDK提供的默认序列化框架,自己开发编解码框架又是个非常复杂的工作,怎么办呢?不用着急,业界有很多优秀的编解码框架,它们在克服了JDK默认序列化框架缺点的基础上,还增加了很多亮点,下面让我们继续了解并学习业界流行的几款编解码框架。

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

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

2.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做编解码框架的原因。

   image.png

    基于Netty+Protobuf实现消息编解码;

2.2.2    Facebook的Thrift介绍

________________________________________

2.2.3    JBossMarshalling介绍

________________________________________

2.2.4    网上序列化对比

   网上有一份,业界常用的序列化框架的对比;

   各种 Java 的序列化库的性能比较测试结果

3    总结

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

参考资料:

 《Netty权威指南》

发表评论