Java循环ByteBuffer实现
[TOC]
网络分包
应用程序需要多次从网络读取数据,每次读取的数据长度不固定,每次读取的数据也不能保证是一个完整的业务报文,那么如何做到读取完整的业务报文呢?这就是网络分包问题。在BIO时代,因为使用的是阻塞式读取,可以读够指定长度的报文再返回。但在NIO、AIO等非阻塞时代,则没办法读取指定的长度。 以常见的NIO Reactor模式为例子,Selector每次select返回后,如果是readable,那么读取的数据可以只读一次,也可以循环读取(只要SocketChannel.read方法返回大于0的数据,并且对应的ByteBuffer还有可用空间): 循环读取示例
ByteBuffer buf = ByteBuffer.allocate(512);
int ret = 0;
int readBytes = 0;
while ((ret = sc.read(buf)) > 0) {
readBytes += ret;
if (!buf.hasRemaining()) { // 如果缓冲区读满,break
break;
}
}
但是需要将每次readable读取返回的ByteBuffer存起来,将里面的数据拼在一块,再按照业务规则分包。可以用一个Queue<ByteBuffer>;也可以用一个大的ByteBuffer,将读取到的ByteBuffer写入大的也可以用一个大的ByteBuffer。这两种方式都会导致过多的ByteBuffer操作,显得不是那么好。 还有一种方式就是使用循环ByteBuffer。
什么是循环ByteBuffer
循环ByteBuffer可以简单地理解成是一个byte[]数组,长度为capacity,首尾相连,构成一个环形,按照操作数组的方向写数据和读数据,可以循环使用。 循环ByteBuffer维护写索引writeIndex和读索引readIndex,分别表示下一个可以写、可以读的byte的数组下标。同时维护一个状态empty,表示是否为空。既然是做为缓存使用,那么当空间不足的时候自动扩容是必要的功能,这里实现的循环ByteBuffer每次扩容为原来的2倍大小。
实现难点
- 存入数据的时候(storeData),有可能数据是连续存储的,也有可能是一部分存在数组尾部,一部分存在数组头部;
- 读取数据的时候(fetchData),也是一样的,可能是连接的,也可能在尾部读一部分,再在头部读一部分;
- 得到空闲空间大小和可读取数据大小时,也需要根据writeIndex和readIndex的大小关系采取不同的处理方式;
- 需要支持预读取数据,即读取过后,不移动readIndex,以便下次仍然从同样位置读取;
代码实现
org.alive.learn.heartbeat.CircleByteBuffer
package org.alive.learn.heartbeat;
import java.util.Arrays;
/**
* <p>
* Cicle Buffer类,底层采用byte[]实现的循环Buffer,支持存取数据,空间不够时长度为自动变为原来的两倍;
*
* <ul>
* <li>用途:可做为Nio读取数据的缓存,实现分包处理;
* <li>本类不是线程安全的;
* <li>为简单起见,未采用DirectMemory,使用的JVM Heap内存;
* <li>因为底层采用的数组存储数据,所以数据有可能不连续;如果按照首尾相联的环形来理解,则是连续的;
* </ul>
*
* @author myumen
* @since 2017.06.28
*/
public class CircleByteBuffer {
/** 默认大小为16字节 */
private final stat