NIO
NIO:non-blocking io 非阻塞 IO
三大组件
Channel & Buffer
Channel 类似于 Stream,它是读写数据的双向通道,可以从 Channel 将数据读入 Buffer,也可以把 Buffer 中的数据写入 Channel;Buffer 则用来缓冲读写数据。
常见的 Channel:FileChannel
、DatagramChannel
、SocketChannel
、ServerSocketChannel
常见的 Buffer:ByteBuffer
(MappedByteBuffer
、DirctByteBuffer
、HeapByteBuffer
)、ShortBuffer
、IntBuffer
、LongBuffer
、FloatBuffer
、DoubleBuffer
、CharBuffer
;最常用的 ByteBuffer
案例
@Slf4j
public class TestByteBuffer {
public static void main(String[] args) {
// 从文件中读取数据
try (FileChannel channel = new FileInputStream("data.txt").getChannel()) {
// 准备缓冲区
ByteBuffer byteBuffer = ByteBuffer.allocate(10); // 给他分配了 10 个字节的空间
while (true) {
// 从 channel 中读取数据,向 byteBuffer 中写
int read = channel.read(byteBuffer);
// 切换为读模式
byteBuffer.flip();
// 打印 byteBuffer 中的数据
while (byteBuffer.hasRemaining()) { // 是否还有剩余数据
log.debug("{}", (char) byteBuffer.get());
}
// 如果为 -1 就代表没有数据了
if (read == -1) break;
// 切换为写模式
byteBuffer.clear();
// byteBuffer.compact();
}
} catch (IOException e) {
log.error("error", e);
}
}
}
上面是通过 输入流 获取 data.txt 文件中内容的一个案例,上面通过 ByteBuffer
把 FileChannel
中的数据读出来。
通过 ByteBuffer.allocate(10)
创建出来一个 HeapByteBuffer
的实例,并且只给它分配 10 个字节的空间。这代表着他一次性只能缓冲 10 个字节。
写完之后通过调用 flip()
方法让 ByteBuffer
变化 读模式 开始输出内容
ByteBuffer
结构图
+---------------------------+
| ByteBuffer |
+---------------------------+
| - mark (标记位置) | // 标记某个特定的位置
| - position (当前位置) | // 当前读/写的位置
| - limit (限制位置) | // 可读/写的最大位置
| - capacity (容量) | // 缓冲区的总容量
| - data (实际存储的数据) | // 存储的字节数组或直接内存
+---------------------------+
mark:用于标记当前的 position
值,方便后续通过 reset()
方法恢复到该位置;默认值为 -1
,表示未设置
position:当前读/写操作的位置;初始值为 0
,随着读写操作逐渐增加
limit:表示缓冲区中可读/写的最大位置。在写模式下,limit
通常等于 capacity
;在读模式下,limit
表示有效数据的末尾
capacity:缓冲区的总容量,表示缓冲区最多可以容纳多少字节。创建时确定,不可更改
data:实际存储的字节数据;数据可以存储在堆内存(HeapByteBuffer
)或直接内存(DirectByteBuffer
)中
现在假设创建了一个 10 字节的 ByteBuffer
状态会经历以下变化:
# 初始状态
+----+----+----+----+----+----+----+----+----+----+
| | | | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+
position = 0, limit = 10, capacity = 10
# 写入 6 字节数据
+----+----+----+----+----+----+----+----+----+----+
| A | B | C | D | E | F | | | | |
+----+----+----+----+----+----+----+----+----+----+
position = 6, limit = 10, capacity = 10
# 切换到读模式
+----+----+----+----+----+----+----+----+----+----+
| A | B | C | D | E | F | | | | |
+----+----+----+----+----+----+----+----+----+----+
position = 0, limit = 6, capacity = 10
# 读取 3 字节后
+----+----+----+----+----+----+----+----+----+----+
| A | B | C | D | E | F | | | | |
+----+----+----+----+----+----+----+----+----+----+
position = 3, limit = 6, capacity = 10
# 重置为写模式(clear)
+----+----+----+----+----+----+----+----+----+----+
| A | B | C | D | E | F | | | | |
+----+----+----+----+----+----+----+----+----+----+
position = 0, limit = 10, capacity = 10
# 重置为写模式(compact)
+----+----+----+----+----+----+----+----+----+----+
| D | E | F | | | | | | | |
+----+----+----+----+----+----+----+----+----+----+
position = 3, limit = 10, capacity = 10
clear 的重置写是把 position 重置为 0 ,到时候新数据来的时候把原来的内容覆盖掉;而 compact 写模式则是把没有读到的部分全部向前压缩,然后切换为写模式之前的数据还是保留的
常用方法
put(byte b)
:写入一个字节(也可以调用 channel
的 read()
方法写入)
get()
:读取一个字节(也可以调用 channel
的 write()
方法读取)
hasRemaining()
:判断当前元素和极限位置之间是否还存在元素
flip()
:切换到读模式,将 limit
设置为当前 position
,并将 position
置为 0
clear()
:重置缓冲区为写模式,position
置为 0,limit
置为 capacity
rewind()
:将 position
置为 0,保持 limit
不变
mark()
& reset()
:标记和恢复 position
实例化
因为 ByteBuffer
本身是一个抽象类所以只能实例化子类,可以通过以下方法:
ByteBuffer.allocate(Number)
-HeapByteBuffer
ByteBuffer.allocateDirect(Number)
-DirectByteBuffer
allocate()
使用 Java 的 堆内存,allocateDirect()
使用的是 直接内存
区别:
- 堆内存:读写效率低、会受到 GC 的影响、分配内存的效率高
- 直接内存:读写效率高(少一次拷贝)、不会受 GC 影响、分配内存的效率低
Selector
Selector 是一个选择器,用来配合一个线程来管理多个 Channel,获取这些 Channel 上发生的事件,这些 Channel 工作在非阻塞模式下,不会让线程死磕在一个 Channel 上,适合连接数特别多但流量低的场景。
调用 Selector 的 select()
会阻塞直到 Channel 发生了读写就绪事件,这些事件发生,select 方法就会返回这些事件交给 thread 来处理