07、Netty学习 - 字节缓冲区 ByteBuf(下)

在了解了 ByteBuffer 的原理之后,再来理解Netty 的 ByteBuf 就比较简单了。

ByteBuf 是 Netty 框架封装的数据缓冲区,区别于 positionlimitflip等属性和操作来控制 ByteBuffer的读写,ByteBuf通过两个位置指针来协助缓冲区的读写操作,分别是readIndexwriteIndex

readIndexwriteIndexcapacity变量存在以下关系:

0 <= readIndex <= writeIndex <= capacity

实现原理

初始化ByteBuffer 时,readIndexwriteIndex 取值一开始都是0。如下图所示:

 

当执行写入数据之后,writeIndex会增加,如下图所示:

 

当执行读入数据之后则会使readIndex增加,但不会超过writeIndex,如下图:

 

在读取之后,索引 0 到 readIndex位置的区域被视为废弃字节(discard)。可以调用discardReadBytes方法,来释放这部分空间,其作用类似于 ByteBuffercompact()方法,移除无用的数据,实现缓冲区的重复利用。如下图,展示了执行discardReadBytes后的情况,相当于可写的空间变大了。

 

ByteBuf 的使用案例

为了更好的理解ByteBuf,编写了以下示例:

public class ByteBufDemo {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// 创建一个缓冲区
		ByteBuf buffer = Unpooled.buffer(10);
		System.out.println("------------初始时缓冲区------------");
		printBuffer(buffer);

		// 添加一些数据到缓冲区中
		System.out.println("------------添加数据到缓冲区------------");

		String s = "love";
		buffer.writeBytes(s.getBytes());
		printBuffer(buffer);

		// 读取数据
		System.out.println("------------读取数据------------");

		while (buffer.isReadable()) {
			System.out.println(buffer.readByte());
		}

		printBuffer(buffer);

		// 执行compact
		System.out.println("------------执行discardReadBytes------------");
		buffer.discardReadBytes();
		printBuffer(buffer);

		// 执行clear
		System.out.println("------------执行clear清空缓冲区------------");
		buffer.clear();
		printBuffer(buffer);

	}

	/**
	 * 打印出ByteBuf的信息
	 * 
	 * @param buffer
	 */
	private static void printBuffer(ByteBuf buffer) {
		System.out.println("readerIndex:" + buffer.readerIndex());
		System.out.println("writerIndex:" + buffer.writerIndex());
		System.out.println("capacity:" + buffer.capacity());
	}
}

输出结果:

------------初始时缓冲区------------
readerIndex:0
writerIndex:0
capacity:10
------------添加数据到缓冲区------------
readerIndex:0
writerIndex:4
capacity:10
------------读取数据------------
108
111
118
101
readerIndex:4
writerIndex:4
capacity:10
------------执行discardReadBytes------------
readerIndex:0
writerIndex:0
capacity:10
------------执行clear清空缓冲区------------
readerIndex:0
writerIndex:0
capacity:10

Process finished with exit code 0

对比ByteBufferByteBuf两个示例可以看出,Netty 提供了更加方便地创建ByteBuf的工具(unpooled),同时,也不必再执行flip()方法来切换读写模式。对比而言,ByteBuf更加易于使用。

ByteBuf 的3种使用模式

ByteBuf 共有三种使用模式:堆缓冲区模式(Heap Buffer)、直接缓冲区模式(Direct Buffer)和 复合缓冲区模式(Composite Buffer)。

堆缓冲模式

堆缓冲区模式又称为支撑数组,其数据是存放在JVM的堆空间,通过将数据存储在数组中实现。

优点:数据存储在JVM堆中可以快速的创建和快速释放,并且提供了数据快速访问的方法;

缺点:每次数据与 I/O 进行传输时,都需要将数据复制到直接缓冲区。

以下是堆缓冲区的代码示例:

public class ByteBufHeapBufferDemo {

	/**
	 * @param args
	 */
	public static void main(String[] args) {
		
		// 创建一个堆缓冲区
		ByteBuf buffer = Unpooled.buffer(10);
		String s = "waylau";
		buffer.writeBytes(s.getBytes());

		// 检查是否是支撑数组
		if (buffer.hasArray()) {

			// 获取支撑数组的引用
			byte[] array = buffer.array();

			// 计算第一个字节的偏移量
			int offset = buffer.readerIndex() + buffer.arrayOffset();

			// 可读字节数
			int length = buffer.readableBytes();
			printBuffer(array, offset, length);
		}
	}

	/**
	 * 打印出Buffer的信息
	 * 
	 * @param buffer
	 */
	private static void printBuffer(byte[] array, int offset, int len) {
		System.out.println("array:" + array);
		System.out.println("array->String:" + new String(array));
		System.out.println("offset:" + offset);
		System.out.println("len:" + len);
	}
}

输出结果:

array:[B@5b37e0d2
array->String:waylau    
offset:0
len:6

Process finished with exit code 0

直接缓冲区模式

直接缓冲区属于堆外分配的直接内存,不会占用堆得空间。

优点:使用 socket 传输数据时性能很好,避免了数据从 JVM 堆内存复制到直接缓冲区的过程,提高了性能。

缺点:相对于堆缓冲区而言,直接缓冲区分配内存空间和释放更为昂贵。

对于涉及大量的 I/O 数据的读写,建议使用直接缓冲区。而对于用于后端业务消息编解码模块,建议使用堆缓冲区。

以下是直接缓冲区代码示例:

public class ByteBufDirectBufferDemo {

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		// 创建一个直接缓冲区
		ByteBuf buffer = Unpooled.directBuffer(10);
		String s = "waylau";
		buffer.writeBytes(s.getBytes());

		// 检查是否是支撑数组.
		// 不是支撑数组,则为直接缓冲区
		if (!buffer.hasArray()) {

			// 计算第一个字节的偏移量
			int offset = buffer.readerIndex();

			// 可读字节数
			int length = buffer.readableBytes();

			// 获取字节内容
			byte[] array = new byte[length];
			buffer.getBytes(offset, array);

			printBuffer(array, offset, length);
		}
	}

	/**
	 * 打印出Buffer的信息
	 * 
	 * @param buffer
	 */
	private static void printBuffer(byte[] array, int offset, int len) {
		System.out.println("array:" + array);
		System.out.println("array->String:" + new String(array));
		System.out.println("offset:" + offset);
		System.out.println("len:" + len);
	}
}

输出结果:

array:[B@6d5380c2
array->String:waylau
offset:0
len:6

Process finished with exit code 0

复合缓冲区模式

复合缓冲区是 Netty 特有的缓冲区。本质上类似于提供一个或多个 ByteBuf 的组合视图,可以根据需要添加和删除不同类型的 ByteBuf

优点:提供了一种访问方式让使用者自由地组合多个ByteBuf,避免了复制和分配新的缓冲区。

缺点:不支持访问其支撑数组。因此如果要访问,需要先将内容复制到堆内存中,再进行访问。

以下示例是复合缓冲区将堆缓冲区和直接缓冲区组合在一起,没有进行任何复制过程,仅仅创建了一个视图而已。

public class ByteBufCompositeBufferDemo {

	/**
	 * @param args
	 */
	public static void main(String[] args) {

		// 创建一个堆缓冲区
		ByteBuf heapBuf = Unpooled.buffer(3);
		String way = "way";
		heapBuf.writeBytes(way.getBytes());

		// 创建一个直接缓冲区
		ByteBuf directBuf = Unpooled.directBuffer(3);
		String lau = "lau";
		directBuf.writeBytes(lau.getBytes());

		// 创建一个复合缓冲区
		CompositeByteBuf compositeBuffer = Unpooled.compositeBuffer(10);
		compositeBuffer.addComponents(heapBuf, directBuf); // 将缓冲区添加到符合缓冲区

		// 检查是否是支撑数组.
		// 不是支撑数组,则为复合缓冲区
		if (!compositeBuffer.hasArray()) {

			for (ByteBuf buffer : compositeBuffer) {
				// 计算第一个字节的偏移量
				int offset = buffer.readerIndex();

				// 可读字节数
				int length = buffer.readableBytes();

				// 获取字节内容
				byte[] array = new byte[length];
				buffer.getBytes(offset, array);

				printBuffer(array, offset, length);
			}

		}
	}

	/**
	 * 打印出Buffer的信息
	 * 
	 * @param buffer
	 */
	private static void printBuffer(byte[] array, int offset, int len) {
		System.out.println("array:" + array);
		System.out.println("array->String:" + new String(array));
		System.out.println("offset:" + offset);
		System.out.println("len:" + len);
	}
}

输出结果:

array:[B@4d76f3f8
array->String:way
offset:0
len:3
array:[B@2d8e6db6
array->String:lau
offset:0
len:3

Process finished with exit code 0

CompositeByteBuf是一个虚拟的缓冲区,其用途是将多个缓冲区显示为单个合并缓冲区,类似数据库中的视图。

总结

通过以上对于ByteBuf的介绍,相信小伙伴们对于ByteBuf的原理也有了一定的了解。下一节我们继续深入Netty的源码。