一、NIO 简介
1. 概述
- 介绍: Java NIO(New Input/Output)是从Java 1.4开始引入的一组新的IO库,旨在替代传统的阻塞式IO。NIO提供了更高效的IO操作,支持非阻塞模式和多路复用,适用于高并发场景。
- 概述:NIO 中通过 Buffer 作为缓存区,Channel 作为数据通道来传输数据进行数据通讯, 通过Selector实现多路复用,一个线程可以管理多个Channel,提高并发性能。Buffer、Channel和Selector 为 NIO 的三个核心组件。
2. 阻塞 VS 非阻塞
- BIO:Blocking I/O(阻塞 IO)。当程序在执行 IO 操作的时候,例如调用一个读操作或一个写操作的时候,线程会进行阻塞直至 IO 操作的成功完成。
- NIO:Non-blocking I/O(非阻塞 IO)。在执行IO操作时,线程不会被阻塞,可以立即返回并继续执行其他任务。线程需要轮询或通过回调机制来检查操作是否完成。
二、 三大组件
1. Buffer
1.1 概念
- Buffer:用来缓冲读写数据
- 读:从通道读取数据(Channel)时,首先将数据读取到Buffer中,一旦Buffer中有了数据,就可以通过 Buffer 中的方法来获取这些数据。
- 写:在向通道(Channel)中写入数据之前,先将数据填充到 Buffer 当中,当 Buffer 填满,需要将其数据刷新 到通道中,然后才能继续填充数据。
- 常用的Buffer类型 :
- ByteBuffer(常用) :存储字节数据(二进制数据)
- DirectByteBuffer(存储在系统内存)
- HeapByteBuffer(存储在 JVM 堆内存)
- IntBuffer: 用于存储整数数据。
- LongBuffer: 用于存储长整型数据。
- FloatBuffer: 用于存储浮点型数据。
- DoubleBuffer: 用于存储双精度浮点型数据。
- CharBuffer: 用于存储字符数据。
- ByteBuffer(常用) :存储字节数据(二进制数据)
- 使用流程:
- 分配 Buffer:使用
Buffer
中的allocate
方法分配一个指定容量的 Buffer。 - 从 Channel 中写入数据:向 Buffer 中写入数据。例如 channel.read(buffer)
- 切换为读模式:使用 Buffer 的
flip()
方法由写模式切换为读模式 - 从 Buffer 读取数据:可以使用 Buffer 提供的
get()
方法 - 切换为写模式(清理 Buffer):使用
clear()
或compact
方法重置 Buffer,以便再次写入数据。clear
:重置postion
为 0,limit 为capacity
。相当于全部清除,无论 Buffer 中的数据是否全部读取完整。compact
:将所有未读的数据移到 Buffer 的开始处,然后将position
设置为未读数据的下一个未知,limit 设置为capacity
。
- 分配 Buffer:使用
capacity 为 Buffer 的容量;position 为下一个要读或写的元素的索引;limit 为 Buffer 中第一个不能读或写的元素索引。
- 示例代码:
import java.nio.ByteBuffer;
public class BufferExample {
public static void main(String[] args) {
// 1. 分配Buffer
ByteBuffer buffer = ByteBuffer.allocate(10);
// 2. 写入数据到Buffer
for (int i = 0; i < buffer.capacity(); i++) {
buffer.put((byte) i);
}
// 3. 准备读操作(切换为读模式)
buffer.flip();
// 4. 读取数据从Buffer
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
// 5. 清理Buffer(重置Buffer以便再次写入数据)
buffer.clear();
// 再次写入数据
for (int i = 10; i < 20; i++) {
buffer.put((byte) i);
}
// 准备读操作(切换为读模式)
buffer.flip();
// 读取数据从Buffer
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
}
}
1.2 Buffer 结构
掌握 Buffer 的结构以及不同状态下的情况有助于从本质上理解 Buffer 具体的操作流程
capacity
:Buffer 的容量,即它能容纳的最大数据量。limit
:Buffer 中第一个不能读或写的元素索引,可以动态改变。position
:下一个要读或写的元素索引。mark
:一个用于记录当前 position 的标记。 通过调用mark()方法设置,调用reset()方法恢复到该位置。
以下为 Buffer 不同状态下的结构:
- 初始化状态:此时 Buffer 为写模式,position 代表第一个要写入的元素的索引,limit 代表的是一旦 position 等于 limit 无法再往 Buffer 中写入数据,表示此时 Buffer 已满。
- 写入数据:往 Buffer 中写入数据 JAVA 四个元素,此时 position 所在的索引 4 为下一个写入数据的索引,(limit - position)代表还可以写入多少个元素。
- 切换为读模式:此时 position 的下一个读取元素的索引,limit 为读取限制索引。
- 读取元素:读取前两个元素,position 指针向前移动两个单位。
- 切换为写模式:
- 方式一: 采用
clear
方法,重置postion
为 0,limit 为capacity
。相当于全部清除,无论 Buffer 中的数据是否全部读取完整。 - 方式二:采用
compact
方法,将所有未读的数据移到 Buffer 的开始处,然后将position
设置为未读数据的下一个未知,limit 设置为capacity
。
- 方式一: 采用
1.3 Buffer 常见 API
1.3.1 基本API
-
allocate(int capacity):分配一个新的Buffer。
-
capacity():返回Buffer的容量。
-
position():返回Buffer的当前位置。
-
limit():返回Buffer的限制。
-
mark():标记当前position。(配合
reset()
使用) -
reset():将position重置为先前标记的位置。
-
clear():清除Buffer,准备重新写入数据。
-
flip():将Buffer从写模式切换到读模式。
-
rewind():重置position为0,准备重新读取数据。
-
compact():将未读的数据移到Buffer的开始位置,然后将position设置为未读数据之后的位置,limit设置为capacity。
-
hasRemaining():判断position和limit之间是否有元素。
1.3.2 数据操作API
-
put(byte b):将一个字节写入Buffer。
-
get():从Buffer读取一个字节。
-
put(byte[] src):将一个字节数组写入Buffer。
-
get(byte[] dst):从Buffer读取字节到一个数组。
-
put(int index, byte b):将一个字节写入Buffer指定位置。
-
get(int index):从Buffer的指定位置读取一个字节。
1.4 API 示例代码
import java.nio.ByteBuffer;
public class BufferAPIExample {
public static void main(String[] args) {
// 分配一个新的ByteBuffer
ByteBuffer buffer = ByteBuffer.allocate(10);
// 写入数据到Buffer
buffer.put((byte) 1);
buffer.put((byte) 2);
buffer.put((byte) 3);
// 使用flip()准备读操作
buffer.flip();
// 读取数据从Buffer
while (buffer.hasRemaining()) {
System.out.println(buffer.get());
}
// 清除Buffer,准备重新写入数据
buffer.clear();
// 再次写入数据到Buffer
buffer.put(new byte[]{4, 5, 6});
// 使用flip()准备读操作
buffer.flip();
// 读取数据从Buffer
byte[] dst = new byte[buffer.remaining()];
buffer.get(dst);
for (byte b : dst) {
System.out.println(b);
}
// 重新标记和重置
buffer.clear();
buffer.put((byte) 7);
buffer.put((byte) 8);
buffer.mark(); // 标记当前位置
buffer.put((byte) 9);
buffer.reset(); // 重置到标记位置
buffer.flip();
System.out.println("After reset: " + buffer.get());
}
}
2. Channel
2.1 概念
- Channel:Channel 是一个可以进行读写操作的通道,类似于流(Stream),但它们的工作方式有很大的不同。Channel 可以同时支持读和写操作(双向),而流通常是单向的(输入流或输出流)。
- Channel 的种类:
- FileChannel:用于文件的读写。
- SocketChannel:用于 TCP 连接读写网络数据。
- ServerSocketChannel:用于监听新进来的 TCP 连接,就像传统的服务器套接字。
- DatagramChannel:用于通过 UDP 读写网络数据。
- 工作原理: Channel 工作在缓冲区(Buffer)上,所有与 Channel 相关的 I/O 操作都通过缓冲区进行。Channel 读取数据时,会将数据放入缓冲区,而写入数据时,会从缓冲区中取出数据。Channel 的非阻塞模式使得它们可以在数据未准备好时立即返回,而不是等待数据准备好。
2.2 文件编程
-
概述:FileChannel 是一个用于文件读写操作的通道,不能直接创建,而是通过
FileInputStream
、FileOutputStream
、RandomAccessFile
或FileChannel.open()
来获取。 -
注意事项:
- FileChannel 只存在阻塞模式,不存在非阻塞模式。
- 网络编程所用到的 Channel 才存在非阻塞模式。
-
常用 API:
-
open:打开一个FileChannel。
-
read:从FileChannel读取数据到Buffer。
-
write:将Buffer中的数据写入FileChannel。
-
position:获取或设置FileChannel的当前位置。
-
size:获取文件的大小。
-
truncate:截取文件到指定大小。
-
force:强制将所有修改写入磁盘。
-
-
示例代码:
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileChannelExample {
public static void main(String[] args) {
try (RandomAccessFile file = new RandomAccessFile("example.txt", "rw");
FileChannel fileChannel = file.getChannel()) {
// 创建Buffer并写入数据
ByteBuffer buffer = ByteBuffer.allocate(1024);
String data = "Hello, FileChannel!";
buffer.put(data.getBytes());
// 准备Buffer进行读操作
buffer.flip();
fileChannel.write(buffer);
// 清空Buffer以便再次写入
buffer.clear();
// 设置Channel位置并读取数据
fileChannel.position(0);
int bytesRead = fileChannel.read(buffer);
System.out.println("读取 " + bytesRead + " 字节");
// 准备Buffer进行读操作
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
2.3 网络编程
-
概述:
ServerSocketChannel
作用于服务器,用于监听客户端的连接请求,SocketChannel
用于客户端和服务器之间的数据传输。 -
ServerSocketChannel 常见 API:
-
open:打开一个
ServerSocketChannel
。 -
bind:绑定到指定的端口。
-
configureBlocking:设置是否为阻塞模式。
-
accept:接受客户端连接。
-
-
SocketChannel 常见 API:
- open:打开一个
SocketChannel
并连接到服务器。 - configureBlocking:设置是否为阻塞模式。
- read:从
SocketChannel
读取数据到ByteBuffer
。 - write:将
ByteBuffer
中的数据写入SocketChannel
。
- open:打开一个
-
示例代码:以下通过实现一个简单的服务器-客户端程序来进行测试,服务器接收客户端发送的数据,并将其原样返回给客户端。
- 服务器程序:其中用到
Selector
组件的内容,在后续内容中会提到。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.channels.SelectionKey;
import java.util.Iterator;
public class EchoServer {
public static void main(String[] args) {
try {
// 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
serverSocketChannel.configureBlocking(false);
// 打开Selector
Selector selector = Selector.open();
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
// 选择准备好的通道
selector.select();
Iterator<SelectionKey> keyIterator = selector.selectedKeys().iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();
if (key.isAcceptable()) {
// 接受连接
SocketChannel clientChannel = serverSocketChannel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
} else if (key.isReadable()) {
// 读取数据
SocketChannel clientChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(256);
int bytesRead = clientChannel.read(buffer);
if (bytesRead == -1) {
clientChannel.close();
} else {
buffer.flip();
clientChannel.write(buffer);
}
}
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
- 客户端程序:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class EchoClient {
public static void main(String[] args) {
try {
// 打开SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
socketChannel.configureBlocking(false);
// 发送消息
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put("Hello, Server!".getBytes());
buffer.flip();
socketChannel.write(buffer);
// 读取服务器的回显
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println("Received from server: " + new String(buffer.array(), 0, bytesRead));
}
// 关闭连接
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
3. Selector
3.1 概念
- Selector:Selector是 Java NIO 的一个重要组件,它允许单个线程监控多个通道(Channel)的 IO 事件,如连接请求、数据到达等。使用Selector,可以实现高效的多路复用,避免每个连接都创建一个线程,从而提高应用的并发性能。
- 多路复用:多路复用允许多个输入/输出通道(例如 Socket)共享一个同一个线程。通过这种方式,可以在一个线程中同时处理多个连接,提高资源利用率和应用的并发性能。在 Java NIO 中,Selector实现了多路复用机制。一个Selector可以同时监控多个Channel的 IO 事件,当某个Channel有事件准备好时,Selector会通知应用程序。
- IO 事件种类:
- OP_ACCEPT:有连接可以接受,服务端成功接受连接时触发该事件(ServerSocketChannel)。
- OP_CONNECT:连接已经建立,客户端成功连接上服务端时触发该事件(SocketChannel)。
- OP_READ:有数据可以读取(SocketChannel)。
- OP_WRITE:可以写数据(SocketChannel)。
- 使用流程:
- 注册通道: 注册 Channel 到 Selector,并指定感兴趣的事件,事件通过SelectionKey对象进行指定(包含了关于通道和 Selector 的信息以及通道感兴趣的操作)。 当有事件发生时,Selector 会将准备好进行 I/O 操作的 SelectionKey 返回给应用程序,然后由应用程序遍历这些 SelectionKey 并处理相应的事件。
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
// 指定感兴趣的事件类型为OP_READ
socketChannel.register(selector, SelectionKey.OP_READ);
- 选择就绪通道: 调用select()方法,阻塞直到至少有一个通道准备好进行I/O操作。
// 返回的是已经就绪的通道的个数
int readyChannels = selector.select();
- 处理就绪事件:遍历返回的SelectionKey集合,确定哪些通道准备好进行I/O操作,并执行相应的操作。
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
// 处理读事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
// 处理读取的数据
}
} else if (key.isAcceptable()) {
// 处理连接接受事件
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
socketChannel.register(selector, SelectionKey.OP_READ);
}
keyIterator.remove();
}
3.2 常见API
- open:打开一个Selector。
- register:将一个Channel注册到Selector上,并指定感兴趣的操作。
- select:阻塞等待直到有至少一个通道准备好进行IO操作。
- selectedKeys:返回一组SelectionKey,对应于准备好的通道。
SelectionKey:代表一个通道在Selector上的注册关系,包含了通道和事件的信息。
3.3 示例代码
知道了 Selector 之后,我们就可以来重写理解上面的那个例子了,以下是关于这个例子的解释。
- 服务器代码:
- 当将 ServerSocketChannel注册到 Selector后,每当有事件发生的时候,Selector获取其中的SelectionKey对象,根据SelectionKey对象可以获取其中的通道对象和具体的事件类型。
- 根据具体的事件类型来执行不同的操作,如果是连接事件,则获取的通道对象为ServerSocketChannel对象,利用该对象可以创建一个新的SocketChannel用来处理服务器和客户端的连接,并将其注册到Selector,并指定感兴趣的事件类型;如果是读事件,此时获取的通道对象为处理服务器和客户端的连接SocketChannel,由于该示例代码是将接受到的数据发送给客户端,所以将从通道中读取的数据重写写入通道给客户端。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class EchoServer {
public static void main(String[] args) throws IOException {
// 创建 selector 对象
Selector selector = Selector.open();
// 创建 serverSocketChannel 对象并进行相应的配置
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.configureBlocking(false);
serverSocketChannel.bind(new InetSocketAddress(8080));
// 将 serverSocketChannel 注册到 selector 并绑定 OP_ACCEPT 事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
while(true){
// 阻塞直到有至少一个通道准备好进行I/O操作
selector.select();
// 获取所有准备好I/O操作的SelectionKey
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while(iterator.hasNext()){
SelectionKey key = iterator.next();
iterator.remove();
if(key.isAcceptable()){
// 有新的连接请求
ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = serverChannel.accept();
socketChannel.configureBlocking(false);
// 注册新连接到Selector,监听读事件
socketChannel.register(selector,SelectionKey.OP_READ);
}else if(key.isReadable()){
// 有数据可以读取
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int byteRead = socketChannel.read(buffer);
if (byteRead == -1) {
socketChannel.close();
}else {
buffer.flip();
socketChannel.write(buffer);
buffer.clear();
}
}
}
}
}
}
- 客户端代码:
- 客户端连接上服务器之后,发送"Hello, Server!"字符串给服务器。
- 发送完毕之后,通过SocketChannel的read方法监听服务器发送过来的数据,最后进行输出。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class EchoClient {
public static void main(String[] args) {
try {
// 打开SocketChannel
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("localhost", 8080));
socketChannel.configureBlocking(false);
// 发送消息
ByteBuffer buffer = ByteBuffer.allocate(256);
buffer.put("Hello, Server!".getBytes());
buffer.flip();
socketChannel.write(buffer);
// 读取服务器的回显
buffer.clear();
int bytesRead = socketChannel.read(buffer);
if (bytesRead > 0) {
buffer.flip();
System.out.println("Received from server: " + new String(buffer.array(), 0, bytesRead));
}
// 关闭连接
socketChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
如果本文对你有帮助的话,希望可以点一个赞,嘻嘻🥰🥰🥰。