核心组件
Channel && Buffer
- channel-通道,buffer-缓存。
- 之前的Java I/O输入输出流都是单向的,要么输入要么输出,NIO引入了channel,它是读写数据的双向通道
- channel可以从channel将数据读入buffer,也可以将buffer的数据写入channel,channel比stream更为底层
- 常见的Channel有
- FileChannel 文件
- DatagramChannel UDP
- SocketChannel TCP 客户端
- ServerSocketChannel TCP服务端
- buffer则用来缓冲读写数据,最常用的事ByteBuffer,常见的buffer有
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
- ByteBuffer
Selector
- Selector-选择器
- 选择器的出现,就是为了实现多个socket的连接
- 多线程的I/O版本缺点:多线程内存占用高;线程上下文切换开销大;只适合连接少的版本。
- 线程池版本I/O缺点:阻塞模式下,线程仅能处理一个socket连接;仅适合短连接场景。
- selector版本:selector的作用就是配合一个线程来管理多个channel,获取这些channel上发生的事件,这些channel工作在非阻塞模式下,不会让线程吊死在一个channel上。适合连接数特别多,但流是低的场景(lowtraffic)。
- 调用selector的select()会阻塞直到channel发生了读写就绪事件,这些事件发生,select方法就会返回这些事件交给thread来处理
ByteBuffer
-
buffer缓冲区的一种,我们最常使用的
-
ByteBuffer正确使用姿势
- 1.向buffer写入数据,例如调用channel.read(buffer)
- 2.调用flip()切换至读模式
- 3.从buffer读取数据,例如调用buffer.get()
- 4.调用clear()或compact()切换至写模式
- 5.重夏1~4步骤
-
ByteBuffer结构
- capacity 容量
- position 写入位置指针
- limit 写入限制指针
-
buffer写入读取流程
- 1.刚开始写入时,
limit
和capacity
一起在尾部,position
在头部 - 2.写入的时候,位置指针
position
一直后移,直到limit
为止 - 3.当写入完成(未写满)时,需要调用
flip()
切换读模式从buffer
里读取数据。此时limit
指针移动至position
位置表示只能读取到这儿,position
指针移至buffer头部从头部开始读取 - 4.当读取完成后,调用
clear()
方法切换至写模式(如果还没读完),position
和limit
归为初始位置(头尾),后续从头开始写入(覆盖写) - 5.当读取到
limit
之前(未读完)就要写入时,需要将未读完的数据(此时position
至limit
)保护起来,因此不能使用clear()
。此时需要调用compact()
方法,将未读完的数据整体前移,position
指针放在未读完数据尾部(limit
移至尾部),从这儿开始写入。
- 1.刚开始写入时,
-
ByteBuffer相关方法
allocate()
,返回堆内存对象HeapByteBuffer
,使用堆内存,读写效率较低,受到GC
影响allocateDirect()
,返回直接内存对象DirectByteBuffer
,使用直接内存(少一次拷贝),读写效率较高,分配内存较慢,不受GC
类型,要防止内存泄漏channel.read(buffer)
从通道读取数据写入bufferbuffer.put((byte)value)
直接往缓冲区放入数据channel.write(buffer)
读取buffer数据写入到通道buffer.get()
从buffer中读取一个字节数据,如果传参也可以读取多个buffer.get(i)
根据索引读取数据(不移动position
)buffer.rewind()
将position
置为0,remark
置为-1,下次重新从头开始读buffer.mark()
对position
位置加标记,与reset()
结合使用buffer.reset()
将position
重置到mark
位置
-
字符串转ByteBuffer
- 使用字符串的
getBytes()
方法,获取byte数组,再使用ByteBuffer
的put()
方法塞进去(注意我们自己put后未调用flip()
,buffer还处于写模式) - 使用
nio
的StandardCharsets.UTF_8.encode("str")
,将字符串直接转为Bytebuffer
(此时已切换成读模式了) - 使用
ByteBuffer
的wrap(byte[])
方法,里面的值,可以使用字符串的getBytes()
方法获取byte数组(此时已切换成读模式了)
- 使用字符串的
-
ByteBuffer转字符串
- 在读模式下,
ByteBuffer
可以直接使用StandardCharsets.UTF_8.decode(buffer).toString()
转化为字符串
- 在读模式下,
-
代码demo
package org.yanyulin;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.charset.StandardCharsets;
/**
* NIO demo
* 从文件中读取内容
*/
public class NioDemo {
public static void main(String[] args) {
// readFile();
scatterRead();
}
/*
* 读取文件内容
*/
private static void readFile() {
try {
// 文件通道,从文件流获取通道
FileChannel fileChannel = new FileInputStream("E:\\nio.txt").getChannel();
// 准备缓冲区
ByteBuffer buffer = ByteBuffer.allocate(10);
StringBuilder builder = new StringBuilder();
// 如果数据未读取结束,一直读取下去
while (true) {
// 从channel里读数据,写入到buffer
int len = fileChannel.read(buffer);
// -1 则读取结束,否则是已读取的字符长度
System.out.println("len = " + len);
if (len == -1) {
break;
}
// 未读取完,切换读模式从buff中读取数据
buffer.flip();
// 按照字节读取buff里的数据
while (buffer.hasRemaining()) {
builder.append((char)buffer.get());
}
// 清空buff,方便后续读取继续往buff写数据,下面继续写模式
buffer.clear();
}
System.out.println(builder.toString());
} catch (IOException e) {
e.printStackTrace();
}
}
/*
* 分散读取文件内容
**/
private static void scatterRead() {
try(FileChannel channel = new RandomAccessFile("E:\\split.txt", "rw").getChannel()){
ByteBuffer buffer1 = ByteBuffer.allocate(8);
ByteBuffer buffer2 = ByteBuffer.allocate(5);
ByteBuffer buffer3 = ByteBuffer.allocate(8);
channel.read(new ByteBuffer[]{buffer1, buffer2, buffer3});
buffer1.flip();
buffer2.flip();
buffer3.flip();
System.out.println(StandardCharsets.UTF_8.decode(buffer1));
System.out.println(StandardCharsets.UTF_8.decode(buffer2));
System.out.println(StandardCharsets.UTF_8.decode(buffer3));
} catch (IOException e) {
e.printStackTrace();
}
}
}