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可以简单地理解成是一个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;
/**
*
* Cicle Buffer类,底层采用byte[]实现的循环Buffer,支持存取数据,空间不够时长度为自动变为原来的两倍;
*
*
*
用途:可做为Nio读取数据的缓存,实现分包处理;*
本类不是线程安全的;*
为简单起见,未采用DirectMemory,使用的JVM Heap内存;*
因为底层采用的数组存储数据,所以数据有可能不连续;如果按照首尾相联的环形来理解,则是连续的;*
*
* @author myumen
* @since 2017.06.28
*/
public class CircleByteBuffer {
/** 默认大小为16字节 */
private final static int DEFAULT_CAPACITY = 16;
/** 存放数据的字节数组 */
private byte[] buf = null;
/** 容量/大小,最多存放字节数 */
private int capacity = 0;
/** 写数据下标,下一个可写的字节对应的数组索引 */
private int writeIndex = 0;
/** 读数据下标,下一个可读的字节对应的数组索引 */
private int readIndex = 0;
/** 是否为空,为空时,writeIndex和readIndex相等,反过来则不一定是空的,也有可能是满的 */
private boolean empty = true;
public CircleByteBuffer() {
this.capacity = DEFAULT_CAPACITY;
this.buf = new byte[capacity];
}
/**
*
* @param cap
* 正整数
*/
public CircleByteBuffer(int cap) {
this.capacity = cap;
this.buf = new byte[cap];
}
public void storeData(byte[] data) {
if (data != null) {
storeData(data, data.length);
}
}
/**
*
* 存放byte[]数据,只存入limit指定长度
*
*
* 如果数据超过可用空间,则进行空间扩展,每次扩展为原来的2倍,直到空间够用;
*
* 存入数据的时候,分为两种情况:
*
* 1. 如果writeIndex >= readIndex,此时可用的存储空间可能有两块:
*
*
(I) 数组尾部,即[writeIndex,* capacity-1]闭区间,长度为capacity-writeIndex,如果够存放,则直接存入数据;不够,则需要将剩余部分存到数组头部;
*
(II) 数组头部,即[0, readIndex-1],长度为readIndex,一定是够存入剩余数据的;因为不够的话前面已经扩容了;*
*
* 2. 如果writeIndex < readIndex,则表示可用空间在数组中间,是连续区域;直接存储就可以了;
*
* 只要有数据存入,那么就一定不为空,empty置为false;
*
* @param data
* 待存入数据
* @param limit
* data中有效数据长度
*/
public void storeData(byte[] data, int limit) {
if (data == null || data.length == 0) {
return;
}
if (limit <= 0 || limit > data.length) {
throw new IllegalArgumentException("参数错误:参数limit值为" + limit + ",超出数组长度范围" + data.length);
}
int writeableBytes = getWriteableBytes();
while (limit > writeableBytes) {
// 数据超过可用空间,空间扩展后,直到够用
this.extendCapaticy();
writeableBytes = getWriteableBytes();
}
// 存入数据
// 可用空间为数组尾部和头部,不连续区域
if (writeIndex >= readIndex) { // 注意条件是大于等于
// 数组尾部可用空间
int arrow = capacity - writeIndex;
if (limit <= arrow) {
// 如果尾部空间够用,直接存到尾部
System.arraycopy(data, 0, buf, writeIndex, limit);
writeIndex += limit;
writeIndex = writeIndex % capacity;
} else {
// 尾部空间不够有,先存入数组尾部,剩余的数据存到数组头部
System.arraycopy(data, 0, buf, writeIndex, arrow);
writeIndex += arrow;
writeIndex = writeIndex % capacity;
// 需要存入数组头部的数据长度
int circle = limit - arrow;
System.arraycopy(data, arrow, buf, writeIndex, circle);
writeIndex += circle;
writeIndex = writeIndex % capacity;
}
} else {
// 可用空间为数组中部连续区域
System.arraycopy(data, 0, buf, writeIndex, limit);
writeIndex += limit;
writeIndex = writeIndex % capacity;
}
empty = false;
}
/**
*
* 提取数据,可根据参数preFetch做预提取。如果无数据,则返回null;如果提取数据长度超过存储的长度,则返回剩下的全部数据。
*
* 数据有可能是连续存放,也有可能不是连续存放,读取也分两种情况:
*
* 1. 如果writeIndex > readIndex,直接读取能够读取到的全部数据;
*
* 2. 如果writeIndex <= readIndex,则说明可读取的数据不连续(如果按照环形来理解,则是连续的),有两块区域:
*
*
(I) 数组尾部,即[readIndex,* capacity-1]闭区间,长度为capacity-readIndex,如果读取的size少于这部分,则直接读取即可;不够,则需要再读取数组头部;
*
(II) 数组头部,即[0, writeIndex-1],长度为writeIndex,最多也只有这么多数据可以读取;*
*
* 如果提取数据长度超过存储的有效数据长度readableBytes,则返回剩下的全部数据;
*
* 如果参数preFetch为true,则表示只读取数据,不移到readIndex,即数据不从缓冲区移出,下次仍然可以读取到。
*
* @param size
* 正整数
* @param preFetch
* 为true表示预读取,读取后数据仍然能再次读取,一般不需要预读取
* @return
*/
public byte[] fetchData(int size, boolean preFetch) {
// 看看还有多少数据可供读取
int readableBytes = getReadableBytes();
// 没有数据,返回null
if (readableBytes == 0) {
return null;
}
// 最终读取的长度
int len = (size >= readableBytes ? readableBytes : size);
byte[] target = new byte[len];
int oldReadIndex = readIndex;
if (writeIndex > readIndex) {
// 数据连续
System.arraycopy(buf, readIndex, target, 0, len);
readIndex += len;
readIndex = readIndex % capacity;
} else {
// 数据不连续
int arrow = capacity - readIndex;
if (len <= arrow) {
System.arraycopy(buf, readIndex, target, 0, len);
readIndex += len;
readIndex = readIndex % capacity;
} else {
int circle = len - arrow;
System.arraycopy(buf, readIndex, target, 0, arrow);
readIndex += arrow;
readIndex = readIndex % capacity;
System.arraycopy(buf, readIndex, target, arrow, circle);
readIndex += circle;
readIndex = readIndex % capacity;
}
}
// 如果是预读取,则需要将readIndex复位,以便下次能继续读取同样的数据
if (preFetch) {
readIndex = oldReadIndex;
}
// 读完数据,如果读写指针相同,则表示读空了
if (readIndex == writeIndex) {
empty = true;
}
return target;
}
/**
* 直接取数据
*
* @param size
* @return
*/
public byte[] fetchData(int size) {
return fetchData(size, false);
}
/**
* 扩容,每次capacity变为原来的2倍
*/
private void extendCapaticy() {
byte[] storedData = fetchData(getReadableBytes(), false);
this.capacity *= 2;
this.clear();
buf = new byte[capacity];
storeData(storedData);
}
/**
* 得到可用存储空间大小
*
* @return 可用存储字节数
*/
public int getWriteableBytes() {
int writeableBytes = 0;
if (writeIndex == readIndex) {
writeableBytes = empty ? capacity : 0;
} else {
writeableBytes = writeIndex > readIndex ? (capacity - writeIndex + readIndex) : (readIndex - writeIndex);
}
return writeableBytes;
}
/**
* 得到可读取的数据长度
*
* @return 可读取字节数
*/
public int getReadableBytes() {
int readableBytes = 0;
if (writeIndex == readIndex) {
readableBytes = empty ? 0 : capacity;
} else {
readableBytes = writeIndex > readIndex ? (writeIndex - readIndex) : (capacity - readIndex + writeIndex);
}
return readableBytes;
}
/**
* 清空Buffer,只将属性重置,具体的buf[]不需要重置
*/
public void clear() {
readIndex = writeIndex = 0;
empty = true;
}
public String toString() {
if (this.buf == null) {
return null;
}
StringBuilder sb = new StringBuilder(capacity + 100);
sb.append("CircleByteBuffer [\n\tbuf=");
sb.append(Arrays.toString(buf));
sb.append("\n\twritePointer:\t" + writeIndex);
sb.append("\n\treadPointer:\t" + readIndex);
sb.append("\n\twriteableBytes:\t" + getWriteableBytes());
sb.append("\n\treadableBytes:\t" + getReadableBytes());
sb.append("\n\tcapacity:\t" + capacity);
sb.append("\n\tempty:\t" + empty);
sb.append("\n]");
return sb.toString();
}
public int getCapacity() {
return capacity;
}
public boolean isEmpty() {
return empty;
}
}
测试代码
必须要说明一下,测试案例有可能不够完全,故源代码也许有BUG,还请注意。
public void testCircleBuffer() {
CircleByteBuffer cb = new CircleByteBuffer(8);
System.out.println(cb);
byte[] store = new byte[] { 'A', 'B', 'C' };
byte[] fetch = null;
cb.storeData(store);
System.out.println(cb);
fetch = cb.fetchData(4, false);
System.out.println(Arrays.toString(fetch)); // 取4字节,但只有3字节,返回3字节['A', 'B', 'C']
System.out.println(cb);
store = new byte[] {'D', 'E', 'F', 'G', 'H', 'I', 'J'};
cb.storeData(store);
System.out.println(cb);
System.out.println(Arrays.toString(cb.fetchData(3)));
System.out.println(cb);
}
public void testExtend() {
CircleByteBuffer cb = new CircleByteBuffer(4);
byte[] store = new byte[] { 'A', 'B', 'C' };
// 自动扩充为8
cb.storeData(store);
cb.storeData(store);
System.out.println(cb);
System.out.println(Arrays.toString(cb.fetchData(5)));
System.out.println(cb);
}
public void testFull() {
CircleByteBuffer cb = new CircleByteBuffer(10);
String req = "你好,收到请求消息一";
cb.storeData(req.getBytes());
String rsp = new String(cb.fetchData(req.getBytes().length));
System.out.println(rsp);
req = "hello, this is message2";
cb.storeData(req.getBytes());
rsp = new String(cb.fetchData(req.getBytes().length));
System.out.println(rsp);
System.out.println(cb.toString());
}
public void testPreFetch() {
CircleByteBuffer cb = new CircleByteBuffer(10);
String req = "ABCDE";
cb.storeData(req.getBytes());
System.out.println(cb.toString());
String rsp = new String(cb.fetchData(req.getBytes().length, true));
System.out.println(rsp);
System.out.println(cb.toString());
cb.fetchData(req.getBytes().length);
System.out.println(cb.toString());
}
自己的码云
源代码存放在自己的码云中 码云:testcase