3.2、Buffer的capacity,position和limit
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.
position
当你写数据到Buffer中时,position表示当前的位置。
当将Buffer从写模式切换到读模式(flip()),position会被重置为0.
当Buffer切换回写模式时(clear()),position重置为0.
当Buffer切换回写模式时(compact()),compact()只会清除已经读过的数据。任何未读过的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面,position重置为缓冲区未读数据的后面.
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。
当切换Buffer到读模式时(flip()), limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。
当Buffer切换回写模式时(clear()),limit重置为capacity.
当Buffer切换回写模式时(compact()),limit重置为capacity.
3.3、Buffer的类型
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
3.4、Buffer的分配
System.out.println(ByteBuffer.allocate(16).getClass()); // 16字节
System.out.println(ByteBuffer.allocateDirect(16).getClass());
// class java.nio.HeapByteBuffer - java 堆内存,读写效率较低,受到 GC 影响
// class java.nio.DirectByteBuffer - 直接内存,读写效率高(少一次拷贝),不会受GC影响,分配的效率低
3.5、向Buffer中写数据
写数据到Buffer有两种方式:
-
从Channel写到Buffer。
int bytesRead = channel.read(buf); //read into buffer.
-
通过Buffer的put()方法写到Buffer里。
buf.put(127);
3.6、从Buffer中读取数据
从Buffer中读取数据有两种方式:
-
从Buffer读取数据到Channel。
int bytesWritten = inChannel.write(buf);
-
使用get()方法从Buffer中读取数据。
byte aByte = buf.get();
3.7、字符串与Buffer互转
字符串转为Buffer
// 1. 字符串转为ByteBuffer,需要手动转换为读模式
ByteBuffer buffer = ByteBuffer.allocate(16);
buffer.put("hello".getBytes());
// 打印buffer中的内容
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println((char)buffer.get());
}
buffer.clear();
// 2. Charset,会自动切换为读模式
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello");
// 打印buffer中的内容
while (buffer2.hasRemaining()) {
System.out.println((char)buffer2.get());
}
buffer2.clear();
// 3. wrap,会自动切换为读模式
ByteBuffer buffer3 = ByteBuffer.wrap("hello".getBytes());
// 打印buffer中的内容
while (buffer2.hasRemaining()) {
System.out.println((char)buffer2.get());
}
buffer2.clear();
Buffer转为字符串
String str2 = StandardCharsets.UTF_8.decode(buffer2).toString(); // 自动切换为读模式
System.out.println(str2);
String str3 = StandardCharsets.UTF_8.decode(buffer3).toString(); // 自动切换为读模式
System.out.println(str3);
buffer.flip(); // 切换为读模式
String str = StandardCharsets.UTF_8.decode(buffer).toString();
System.out.println(str);
3.8、Buffer的一些方法
rewind()方法
Buffer.rewind()将position设回0,所以你可以重读Buffer中的所有数据。limit保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
get方法会让position往后走,如果想重复的读取数据:
- 可以调用rewind方法将position重新置为0
- 或者调用get(int i)方法获取索引i的内容,他不会移动position
mark()与reset()方法
通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
buffer.mark(); // 加标记
buffer.reset(); // 将position重置到 标记处
get(i)
调用get(int i)方法获取索引i的内容,他不会改变position的位置
4、Scatter/Gather
分散度集中写,针对Channel
Java NIO开始支持scatter/gather,scatter/gather用于描述从Channel中读取或者写入到Channel的操作。
分散(scatter)从Channel中读取是指在读操作时将读取的数据写入多个buffer中。因此,Channel将从Channel中读取的数据“分散(scatter)”到多个Buffer中。
聚集(gather)写入Channel是指在写操作时将多个buffer的数据写入同一个Channel,因此,Channel 将多个Buffer中的数据“聚集(gather)”后发送到Channel。
scatter / gather经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的buffer中,这样你可以方便的处理消息头和消息体。
4.1、Scattering Reads
try (FileChannel channel = new RandomAccessFile("data/nio-data.txt", "r").getChannel()) {
ByteBuffer b1 = ByteBuffer.allocate(10);
ByteBuffer b2 = ByteBuffer.allocate(3);
ByteBuffer b3 = ByteBuffer.allocate(3);
channel.read(new ByteBuffer[]{b1,b2,b3});
//打印b1,b2,b3的内容
printBuffer(b1);
printBuffer(b2);
printBuffer(b3);
} catch (IOException e) {
}
private static void printBuffer(ByteBuffer buffer) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.println((char) buffer.get());
}
buffer.clear();
}
Scattering Reads在移动下一个buffer前,必须填满当前的buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads才能正常工作。
4.2、Gathering Writes
ByteBuffer buffer1 = StandardCharsets.UTF_8.encode("hello1");
ByteBuffer buffer2 = StandardCharsets.UTF_8.encode("hello2");
ByteBuffer buffer3 = StandardCharsets.UTF_8.encode("hello3");
try (FileChannel channel = new RandomAccessFile("data/nio-data.txt", "rw").getChannel()) {
channel.write(new ByteBuffer[]{buffer1,buffer2,buffer3});
} catch (IOException e) {
}
注意只有position和limit之间的数据才会被写入。因此,如果一个buffer的容量为128byte,但是仅仅包含58byte的数据,那么这58byte的数据将被写入到channel中。
5、粘包/半包分析
例题:网络上有多条数据发送给服务器,数据之间使用 \n 进行分割
但由于某种原因这些数据在接收时,被进行了重新组合,假如原始数据有3条:
Hello,world\n
I'm Wumin\n
How are you?\n
变成了下面的两个 ByteBuffer (粘包,半包)
Hello,world\nI'm Wumin\nHo
w are you?\n
-
粘包:两条消息合在一起。发送消息时,一次将全部消息发送出去,就可能产生黏包问题。
-
半包:消息被截断。半包主要是因为服务器缓冲区导致的,放不下了,所以就会产生半包。
现在要求你编写程序,将错乱的数据恢复成原始的按 \n 分隔的数据
public class TestBufferExam {
public static void main(String[] args) {
/**
* 网络上有多条数据发送给服务器,数据之间使用 \n 进行分割
*
* 但由于某种原因这些数据在接收时,被进行了重新组合,假如原始数据有3条:
*
* Hello,world\n
*
* I'm Wumin\n
*
* How are you?\n
*
* 变成了下面的两个 ByteBuffer (==粘包,半包==)
*
* Hello,world\nI'm Wumin\nHo
*
* w are you?\n
* 现在要求你编写程序,将错乱的数据恢复成原始的按 \n 分隔的数据
*/
ByteBuffer source = ByteBuffer.allocate(40);
source.put("Hello,world\nI'm Wumin\nHo".getBytes());
split(source);
source.put("w are you?\n".getBytes());
split(source);
}
private static void split(ByteBuffer source) {
source.flip();
for (int i = 0; i < source.limit(); i++) {
if (source.get(i) == '\n') {
int length = i + 1 - source.position();
ByteBuffer target = ByteBuffer.allocate(length);
for (int j = 0; j < length; j++) {
target.put(source.get());
}
System.out.println(source.position());
printBuffer(target);
}
}
source.compact();
}
private static void printBuffer(ByteBuffer buffer) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
}
}
6、FileChannel
FileChannel无法设置为非阻塞模式,它总是运行在阻塞模式下
我们无法直接打开一个FileChannel,需要通过使用一个InputStream、OutputStream或RandomAccessFile来获取一个FileChannel实例,他们都有getChannel()方法
- 通过InputStream获取的channel只能读
- 通过OutputStream获取的channel只能写
- 通过RandomAccessFile获取的channel根据构造RandomAccessFile时的读写模式决定