一、NIO 的介绍
1.1、什么是 Java NIO?
Java NIO (No-blocking IO) 一种同步非阻塞的I/O模型, 为了补充Java标准IO(BIO) 的不足在Java 1.4 中引入,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的IO模型。
1.2、如何理解BIO、NIO、AIO的区别?
- Java BIO:同步并阻塞(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不作任何事情会造成不必要的线程开销。
- Java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的连接请求会被注册到多路复用器上,多路复用器轮询到有 I/O 请求就会进行处理。
- Java AIO:异步非阻塞,AIO 引入了异步通道的概念,采用了 Proactor 模式,简化了程序编写,有效的请求才启动线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间较长的应用。
二、NIO 相关概念
2.1、流与块
I/O 与 NIO 最重要的区别是数据打包和传输的方式,I/O 以流的方式处理数据,而 NIO 以块的方式处理数据。
面向流的 I/O 一次处理一个字节数据: 一个输入流产生一个字节数据,一个输出流消费一个字节数据。为流式数据创建过滤器非常容易,链接几个过滤器,以便每个过滤器只负责复杂处理机制的一部分。不利的一面是,面向流的 I/O 通常相当慢。
面向块的 I/O 一次处理一个数据块,按块处理数据比按流处理数据要快得多。但是面向块的 I/O 缺少一些面向流的 I/O 所具有的优雅性和简单性。
I/O 包和 NIO 已经很好地集成了,java.io. 已经以 NIO 为基础重新实现了,所以现在它可以利用 NIO 的一些特性。例如,java.io. 包中的一些类包含以块的形式读写数据的方法,这使得即使在面向流的系统中,处理速度也会更快。
2.2、Buffer(缓冲区)
- 定义
Buffer对象可以被称为有固定容量的容器。它充当储存罐或临时暂存区,可以存储数据并在以后检索数据。缓冲器与通道密切配合。通道是进行I / O传输的实际门户; 缓冲区是这些数据传输的来源或目标。对于向外传输,您要发送的数据将放在缓冲区中,该缓冲区将传递到输出通道。对于向内传输,通道将数据存储在您提供的缓冲区中,然后将数据从缓冲区复制到通道中。在协作对象之间切换缓冲区是NIO API下有效数据处理的关键。
- 类图
Buffer 的特点
- 缓冲区的容量是固定的(在创建Buffer时,初始化容量)
- 默认情况下,缓冲区是只读的,只有选中的缓冲区是可写的
- 缓冲区是通道的端点
- 在只读的缓冲区中, 其保存的内容是不可变的,但缓冲区的属性是可变的
- 默认情况下,缓冲区是线程不安全的
Buffer 的数据结构(线性表形式)
Buffer 属性介绍
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
- Capacity: 缓冲区的最大容量,在缓冲区创建时初始化,初始化后不能更改
- Limit: 缓冲区可读写的范围, 即缓冲区实际数据大小
- Position: 指出当前缓冲区可读或可写的位置,在缓冲区创建时初始化为0
- Mark: 用于保存Position 位置,调用mark() 方法保存当前读取或写入的位置:mark = position, 调用reset( ) 方法恢复读取或写入的位置:position = mark。
- Buffer 的操作图示
2.3、Channel(通道)
- Channel 与 Buffer 交互图示
- 定义
Channel是一种在字节缓冲区(Buffer)和通道另一端的实体(数据源)(通常是文件(磁盘IO)或套接字socket(网络IO))之间有效传输数据的媒介。
Channel 接口
public interface Channel extends Closeable {
//通道是否已开启
public boolean isOpen();
//用于关闭通道
public void close() throws IOException;
}
类层次结构图
- 文件拷贝例子 ```java //创建输入流 FileInputStream input = new FileInputStream (“/home/hdj/IdeaProjects/demoio/src/main/resources/copyToFile.txt”); //获取读取通道 ReadableByteChannel source = input.getChannel();
//创建输出流 FileOutputStream output = new FileOutputStream (“/home/hdj/IdeaProjects/demoio/src/main/resources/copyToFile4.txt”); //获取写入通道 WritableByteChannel dest = output.getChannel();
//创建直接缓冲区 ByteBuffer buffer = ByteBuffer.allocateDirect(16 * 1024);
while (source.read(buffer) != -1) { // 转换缓冲区为可读模式 //即使用limit 变量指定可读区域(范围position < read < limit) buffer.flip();
// 保证读取全部缓冲区中的元素
while (buffer.hasRemaining())
{
dest.write(buffer);
}
//标识缓冲区为空状态,表示可写入
//position =0, limit=capacity
//数据实际上并没有置空
buffer.clear();
} //关闭通道 source.close(); dest.close();
<a name="U9daU"></a>
###
<a name="wiATs"></a>
### 2.4、Selector (选择器)
- 定义
Selector 是一个可选择通道的多路复用器,用作可以进入非阻塞模式的特殊通道(Channel), 它可以检查一个或多个NIO通道(Channel),并确定哪个通道准备好进行通信,即读或写。
那为什么要使用Selector 呢?原因是 Selector 用于单线程处理多个通道;因此,selector 能使用较少的线程来处理多个通道(Channel), 因为线程的切换比较耗资源,因此可用Selector 来提高效率。
- 图示
![](https://cdn.nlark.com/yuque/0/2022/png/438760/1642058279694-7cd534fe-0a3c-4000-8aae-3c4255be615f.png#clientId=uc16e80cd-82bd-4&crop=0&crop=0&crop=1&crop=1&from=paste&id=u01981d4b&margin=%5Bobject%20Object%5D&originHeight=403&originWidth=541&originalType=url&ratio=1&rotation=0&showTitle=false&status=done&style=none&taskId=uf9a161db-1fca-4926-94e6-e55cfb8ba90&title=)
- NIO Socket 操作
```java
public class NIOServer {
public static void main(String[] args) throws IOException {
//创建选择器
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
//channel 注册选择器模式
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
ServerSocket serverSocket = ssChannel.socket();
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 8888);
serverSocket.bind(address);
while (true) {
selector.select();
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
// 服务器会为每个新连接创建一个 SocketChannel
SocketChannel sChannel = ssChannel1.accept();
sChannel.configureBlocking(false);
// 这个新连接主要用于从客户端读取数据
sChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
SocketChannel sChannel = (SocketChannel) key.channel();
System.out.println(readDataFromSocketChannel(sChannel));
sChannel.close();
}
keyIterator.remove();
}
}
}
private static String readDataFromSocketChannel(SocketChannel sChannel) throws IOException {
ByteBuffer buffer = ByteBuffer.allocate(1024);
StringBuilder data = new StringBuilder();
while (true) {
buffer.clear();
int n = sChannel.read(buffer);
if (n == -1) {
break;
}
buffer.flip();
int limit = buffer.limit();
char[] dst = new char[limit];
for (int i = 0; i < limit; i++) {
dst[i] = (char) buffer.get(i);
}
data.append(dst);
buffer.clear();
}
return data.toString();
}
}
public class NIOClient {
public static void main(String[] args) throws IOException {
Socket socket = new Socket("127.0.0.1", 8888);
OutputStream out = socket.getOutputStream();
String s = "hello world";
out.write(s.getBytes());
out.close();
}
}
2.4、MappeteBuffer
内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快得多。 向内存映射文件写入可能是危险的,只是改变数组的单个元素这样的简单操作,就可能会直接修改磁盘上的文件。修改数据与将数据保存到磁盘是没有分开的。