Java NIO (New IO) 是 Java 对 IO API的替代,即标准 Java IO 和 Java Networking API 的替代。 Java NIO 提供了与传统 IO API 不同的 IO 编程模型。注意:有时 NIO 被称为非阻塞 IO。然而,这并不是NIO最初的意思。此外,部分 NIO API 实际上是阻塞的 ——例如文件 API ,所以标签“非阻塞”会有一点误导。
关键字:非阻塞IO,Channels和Buffers,Selectors
非阻塞IO
Java NIO 使您能够进行非阻塞 IO操作。例如,线程可以通过通道将数据读入缓冲区。当通道将数据读入缓冲区时,线程可以做其他事情。一旦数据被读入缓冲区,线程就可以继续处理它。将数据写入通道也是如此。
Channels和Buffers
在标准 IO API 中,您可以使用字节流和字符流。在 NIO 中,您可以使用通道和缓冲区。数据总是从通道读入缓冲区,或从缓冲区写入通道。
Selectors
Java NIO 包含“选择器”的概念。选择器是一个可以监视多个事件通道的对象(例如:连接打开、数据到达等)。因此,单个线程可以监视多个通道的数据。
1.NIO概述
Java NIO 由以下核心组件组成:
- Channels
- Buffers
- Selectors
Java NIO 拥有比这些更多的类和组件,但在我看来,Channel、Buffer 和 Selector 构成了 API 的核心。 其余的组件,如 Pipe 和 FileLock,只是与三个核心组件结合使用的实用程序类。 因此,我将在本 NIO 概述中重点介绍这三个组件。
Channel和Buffer
通常,NIO 中的所有 IO 都以一个 Channel 开头。 Channel 有点像流。 可以从 Channel 中读取数据到 Buffer 中。 数据也可以从缓冲区写入通道。 如下图:
以下是 Java NIO 中主要 Channel 实现的列表:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
可以看到,这些通道涵盖了UDP+TCP网络IO和文件IO。
这是 Java NIO 中核心 Buffer 实现的列表
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
这些缓冲区涵盖了您可以通过 IO 发送的基本数据类型:字节、短整型、整型、长整型、浮点型、双精度和字符。Java NIO 还有一个 MappedByteBuffer,它与内存映射文件一起使用。
Selectors
Selector 允许单个线程处理多个 Channel。 如果您的应用程序打开了许多连接(通道),但每个连接上的流量很低,使用Seector会很方便。 比如在聊天服务器中。
下图是使用选择器处理 3 个通道的线程:
要使用Selector,您可以使用它注册Channel。 然后调用它的 select() 方法。,此方法将阻塞,直到为注册通道之一准备好事件。 一旦方法返回,线程就可以处理这些事件。
2.Java NIO Channel
Java NIO Channels 类似于流,但有一些不同:
- 您可以读取和写入通道,流通常是单向的(读或写);
- 通道可以异步读取和写入;
-
通道总是读取或写入缓冲区;
如上所述,您将数据从通道读取到缓冲区,并将数据从缓冲区写入通道。
image.png
以下是 Java NIO 中最重要的 Channel 实现:
- FileChannel
- DatagramChannel
- SocketChannel
- ServerSocketChannel
FileChannel 从文件读取数据和向文件读取数据。
DatagramChannel 可以通过 UDP 在网络上读写数据。
SocketChannel 可以通过 TCP 在网络上读写数据。
ServerSocketChannel 允许您侦听传入的 TCP 连接,就像 Web 服务器一样。 对于每个传入连接,都会创建一个 SocketChannel。
下面是一个使用 FileChannel 将一些数据读入 Buffer 的基本示例:
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
/**
* Created by wk on 2021/6/30.
* 版权归 WK 所有
*/
public class BasicChannelExample {
public static void main(String[] args) throws Exception {
RandomAccessFile accessFile = new RandomAccessFile("D:\\Users\\wk\\IdeaProjects\\mall\\test\\src\\main\\resources\\data\\nio-data.txt", "rw");
FileChannel inChannel = accessFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(2);
int bytesRead = inChannel.read(buf);//read into buffer
while (bytesRead != -1) {
// System.out.println("Read " + bytesRead);
buf.flip(); // make buffer ready for read
//hasRemaining()返回值:当且仅当此缓冲区中至少剩余一个元素时,此方法才会返回true。
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.clear();
bytesRead = inChannel.read(buf);
}
accessFile.close();
}
}
3.Java NIO Buffer
Java NIO 缓冲区在与 NIO 通道交互时使用。 如您所知,数据从通道读取到缓冲区,然后从缓冲区写入通道。
缓冲区本质上是一块内存,您可以在其中写入数据,然后您可以再次读取数据。 这个内存块被包装在一个 NIO Buffer 对象中,它提供了一组方法,可以更容易地使用内存块。
Buffer基本用法
使用 Buffer 读取和写入数据通常遵循以下 4 个小步骤:
- 1.将数据写入缓冲区
- 2.调用 buffer.flip()
- 3.从缓冲区中读取数据
- 4.调用 buffer.clear() 或 buffer.compact()
当您将数据写入缓冲区时,缓冲区会跟踪您已写入的数据量。一旦需要读取数据,就需要使用 flip() 方法调用将缓冲区从写入模式切换到读取模式。在读取模式下,缓冲区允许您读取写入缓冲区的所有数据。
读取完所有数据后,您需要清除缓冲区,使其准备好再次写入。您可以通过两种方式执行此操作:通过调用 clear() 或通过调用 compact()。 clear() 方法清除整个缓冲区, compact() 方法只清除您已经读取的数据。任何未读数据都被移到缓冲区的开头,数据将在未读数据之后写入缓冲区。
一个简单的 Buffer 使用示例:
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();
//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
buf.flip(); //make buffer ready for read
while(buf.hasRemaining()){
System.out.print((char) buf.get()); // read 1 byte at a time
}
buf.clear(); //make buffer ready for writing
bytesRead = inChannel.read(buf);
}
aFile.close();
Buffer Capacity, Position 和 Limit
Buffer 需要熟悉的三个属性,以便了解 Buffer 的工作原理:
- capacity
- position
- limit
position 和 limit 的含义取决于 Buffer 是处于读模式还是写模式,无论Buffer模式如何,capacity大小始终是相同的。
下图是写入和读取模式下的capacity、position和limit的说明:
capacity
作为一个内存块,Buffer有一个固定的大小值,也叫“capacity”.你只能往里写capacity个byte、long,char等类型。一旦Buffer满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据。
position
当你写数据到Buffer中时,position表示当前的位置。初始的position值为0.当一个byte、long等数据写到Buffer后, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。
当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0。当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit
在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。
当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据(limit被设置成已写数据的数量,这个值在写模式下就是position)
Buffer 类型
- ByteBuffer
- MappedByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
如您所见,这些 Buffer 类型代表不同的数据类型。换句话说,它们让您可以将缓冲区中的字节作为 char、short、int、long、float 或 double 进行处理。
Allocating a Buffer
要获得 Buffer 对象,您必须首先分配它。每个 Buffer 类都有一个allocate() 方法来执行此操作。这是一个 ByteBuffer 分配的示例,容量为 48 字节:
ByteBuffer buf = ByteBuffer.allocate(48);
这是一个为 CharBuffer 分配 1024 个字符空间的示例:
CharBuffer buf = CharBuffer.allocate(1024);
将数据从通道写入缓冲区
通过缓冲区的 put() 方法自己将数据写入缓冲区。下面是一个示例,展示了 Channel 如何将数据写入缓冲区:
int bytesRead = inChannel.read(buf); //读入缓冲区。
下面是一个通过 put() 方法将数据写入 Buffer 的示例:
buf.put(127);
put() 方法还有许多其他版本,允许您以多种不同方式将数据写入缓冲区。例如,在特定位置写入,或将字节数组写入缓冲区。有关具体缓冲区实现的更多详细信息,请参阅 JavaDoc。
flip()
flip() 方法将 Buffer 从写入模式切换到读取模式。 调用 flip() 将postition设置回 0,并将limit设置为position刚刚所在的位置。
从缓冲区读取数据
有两种方法可以从缓冲区读取数据:
- 将缓冲区中的数据读入通道;
- 使用 Buffer 的某一个get() 方法,buffer自己从缓冲区读取数据。
以下是如何将数据从缓冲区读取到通道的示例:
//read from buffer into channel.
int bytesWritten = inChannel.write(buf)
byte aByte = buf.get();
rewind()——倒带
Buffer.rewind() 将position设置回 0,因此您可以重新读取缓冲区中的所有数据。limit保持不变,因此仍然标记可以从缓冲区读取的元素(字节、字符等)的数量。
//rewind()源码
public final Buffer rewind() {
position = 0;//重置下一个要读取的元素索引为0,表示从头读取
mark = -1;//清除mark,表示之前有保存的临时位置不能用了
return this;//返回当前实例
}
clear() 和compact()
完成从 Buffer 中读取数据后,您必须让 Buffer 准备好再次写入。您可以通过调用 clear() 或调用 compact() 来实现。
相同点:
调用完compcat和clear方法之后的buffer对象一般都是继续往该buffer中写入数据的。不同点:
(1)clear是把position=0,limit=capcity等,也就是说,除了内部数组,其他属性都还原到buffer创建时的初始值,而内部数组的数据虽然没赋为null,但只要不在clear之后误用buffer.get就不会有问题,正确用法是使用buffer.put从头开始写入数据;
(2)而compcat是把buffer中内部数组剩余未读取的数据复制到该数组从索引为0开始,然后position设置为复制剩余数据后的最后一位元素的索引+1,limit设置为capcity,此时在0 ~ position之间是未读数据,而position ~ limit之间是buffer的剩余空间,可以put数据。
mark() 和reset()
您可以通过调用 Buffer.mark() 方法来标记 Buffer 中的给定位置。然后,您可以稍后通过调用 Buffer.reset() 方法将位置重置回标记位置。下面是一个例子:
buffer.mark();
//call buffer.get() a couple of times, e.g. during parsing.
buffer.reset(); //set position back to mark.
equals() 和compareTo()
可以使用 equals() 和 compareTo() 比较两个buffer对象。
equals()方法
当满足下列条件时,表示两个Buffer相等:
- 有相同的类型(byte、char、int等)。
- Buffer中剩余的byte、char等的个数相等。
- Buffer中所有剩余的byte、char等都相同。
-如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。
compareTo()方法
compareTo() 方法比较两个缓冲区的剩余元素(字节、字符等),例如用于排序。在以下情况下,缓冲区被认为比另一个缓冲区“小”:
- 第一个不相等的元素小于另一个Buffer中对应的元素 。
- 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。