NIO 基础
NIO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
三大组件
1.1 Channel (通道)
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。
常见channel
FileChannel
DatagramChannel
socketChannel
ServerSocketChannel
1.2 Buffer (内存缓冲区)
Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
graph LR
channel --> buffer
buffer --> channel
常见Buffer
ByteBuffer
MappedByteBuffer
DirectByteBuffer
HeapByteBuffer
ShortBuffer
IntBuffer
LongBuffer
FloatBuffer
DoubleBuffer
1.3 Selector (选择器)
Selector是一个ava NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率.
多线程版设计
graph TD
subgraph 多线程版
t1(thread) --> s1(socket1)
t2(thread) --> s2(socket2)
t3(thread) --> s3(socket3)
end
⚠️ 多线程版缺点
- 内存占用高
- 线程上下文切换成本高
- 只适合连接数少的场景
线程池版设计
graph TD;
subgraph 线程池版
t4(thread) --> s4(socket1);
t5(thread) --> s5(socket2);
t4(thread) -.-> s6(socket3);
t5(thread) -.-> s7(socket4);
end;
⚠️ 线程池版缺点
- 阻塞模式下,线程仅能处理一个 socket 连接
- 仅适合短连接场景
selector 版设计
selector 的作用就是配合一个线程来管理多个 channel,获取这些 channel 上发生的事件,这些 channel 工作在非阻塞模式下,不会让线程吊死在一个 channel 上。适合连接数特别多,但流量低的场景(low traffic)
graph TD
subgraph selector 版
thread --> selector
selector --> c1(channel)
selector --> c2(channel)
selector --> c3(channel)
end
每个channel都会对应一个 Buffer一个线程对应Selector ,一个Selector对应多个channel(连接)程序切换到哪个channel是由事件决定的Selector 会根据不同的事件,在各个通道上切换Buffer 就是一个内存块,底层是一个数组数据的读取写入是通过 Buffer完成的,BlO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写。
Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到lO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存取数据
2. ByteBuffer
使用 FileChannel 来读取文件内容
@Slf4j
public class ChannelDemo1 {
public static void main(String[] args) {
String fileUri = "C:/1.txt";
try (RandomAccessFile file = new RandomAccessFile(fileUri, "rw")) {
FileChannel channel = file.getChannel();
//ByteBuffer buffer = ByteBuffer.allocate(10); // 堆内存读写
ByteBuffer buffer = ByteBuffer.allocateDirect(10); //直接内存读写, 少一次用户态的拷贝,不受GC 影响。 netty底层采用此方法
do {
int len = channel.read(buffer);
if (len == -1) {
break;
}
// 读模式
buffer.flip();
while(buffer.hasRemaining()) {
system.out.print(buffer.get())
}
// 写模式
buffer.clear();
} while (true);
} catch (IOException e) {
e.printStackTrace();
}
}
}
new RandomAccessFile(fileUri, "rw") rw代表读写权限
FileInputStream 获取的 channel 只能读 channel.read(buffer)
FileOutputStream 获取的 channel 只能写 channel.write(buffer)
ByteBuffer.allocate(10); // 堆内存读写 JVM处理 受GC的影响,有 JVM 调度管理
ByteBuffer.allocateDirect(10); //直接操作系统函数内存读写, 少一次用户态的拷贝,不受GC 影响。 netty底层采用此方法。 ❗使用切记回收,以防止内存泄漏
上文中读写模式解析
初始结构
写入模式 abcd
字符串
此时此刻,position 已经到下标第4。如果直接读取,则数据想走后一直为 NULL。需要切换读写模式,将position重新切换到下标为0
flip 切换读模式 = 切换 position 和 limit下标
compact 切换到写模式
clear 动作,恢复到初始结构
get() 读取当前position值,position 下标 后移
get(i) 读取当前position值,position 下标不变
flip 读取当前position值,position 下标 变为0 limit改为position。
rewind 读取当前position值,position 下标 变为0。
mark 做一个标记,记录 position 位置(rewind的增强)
reset 是将 position 重置到 mark 的位置(rewind的增强)
粘包和半包
粘包问题发生在 TCP/IP 协议中,因为 TCP 是面向连接的传输协议,它是以“流”的形式传输数据的,而“流”数据是没有明确的开始和结尾边界的,所以就会出现粘包问题。
所谓的粘包问题是指数据在传输时,在一条消息中读取到了另一条消息的部分数据,这种现象就叫做粘包。比如发送了两条消息,分别为“ABC”和“DEF”,那么正常情况下接收端也应该收到两条消息“ABC”和“DEF”,但接收端却收到的是“ABCD”,像这种情况就叫做粘包,如下图所示:
半包问题是指接收端只收到了部分数据,而非完整的数据的情况就叫做半包。比如发送了一条消息是“ABC”,而接收端却收到的是“AB”和“C”两条信息,这种情况就叫做半包,如下图所示:
半包问题可以用:compact
评论区