NIO
一、NIO概述
是从 Java 1.4 版本开始引入的一个新的IO API,可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。
1. 阻塞IO
- 通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同 样,写入调用将会阻塞直至数据能够写入
2. 非阻塞IO(NIO)
- NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反 是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定 事件时,系统再通知我们
- 理解起来就是:NIO全双工,双向的,阻塞IO只能是单向的
3. 核心部分
- Channels
- Buffers
- Selectors
二、Channels
- Channel 是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过 Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上 移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于 读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底 层操作系统的 API。
1 FileChannel
- FileChannel从文件中读写数据
1.1 方法介绍
方法 | 介绍 |
---|---|
int read(ByteBuffer dst) | 从Channel中读取数据到ByteBuffer |
long read(ByteBuffer[] dsts) | 将Channel中的数据“分散”到ByteBuffer[] |
int write(ByteBuffer src) | 将ByteBuffer中的数据写入到Channel |
long position() | 返回次通道的文件位置 |
FileChannel position(long p) | 设置次通道的文件位置 |
long size() | 返回此通道的文件的当前大小 |
FileChannel truncate(long s) | 将此通道的文件截取为给定大小 |
void fore(boolean metaData) | 强制将所有对此通道的文件更新写入到存储设备中 |
1.2 读文件
@Test
public void read() throws Exception {
//打开一个通道(InputStream/OutputStream/RandomAccessFile)来获取一个FileChannel的实例
RandomAccessFile randomAccessFile = new RandomAccessFile("D:\\aa.txt","rw");
FileChannel inchannel = randomAccessFile.getChannel();
//new一个缓冲区,设置大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
//将inchannel中的数据读取到buffer(代表由多少字节被读到了buffer中,-1表示到了文件末尾)
int bytesRead = inchannel.read(buffer);
//当返回-1的时候代表读完
while (bytesRead!=-1){
System.out.println("读取"+bytesRead);
//反转此缓冲区,读写转换
buffer.flip();
//判断缓冲区是否还有剩余
while (buffer.hasRemaining()){
System.out.print((char)buffer.get());
}
//缓冲区清空
buffer.clear();
//清空后返回bytesRead=-1,退出循环,不写这行死循环
bytesRead = inchannel.read(buffer);
}
randomAccessFile.close();
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QY2F2WtH-1669130362008)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211002110822392.png)]
1.3 写文件
@Test
public void write() throws Exception{
//打开一个通道(InputStream/OutputStream/RandomAccessFile)来获取一个FileChannel的实例
RandomAccessFile aFile = new RandomAccessFile("D:\\bb.txt","rw");
FileChannel inChannel = aFile.getChannel();
//要准备写入的数据
String newData = "今天天气不错"+System.currentTimeMillis();
//new一个缓冲区,设置大小
ByteBuffer buffer = ByteBuffer.allocate(1024);
//清空缓冲区
buffer.clear();
buffer.put(newData.getBytes());
//反转读写模式
buffer.flip();
//判断缓冲区是否还有剩余
while (buffer.hasRemaining()){
//写入数据
inChannel.write(buffer);
}
inChannel.close();
}
2 Socket通道
2.1 通道的定义
- 通道是一个连接 I/O 服务导管并提供与该服务交互的方法。就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API,而 java.net 中 已经存在的 socket 通道都可以被大多数协议操作重复使用。
2.2 全部socket通道类
- ServerSocketChannel
- SocketChannel
- DatagramChannel
2.2.1 ServerSocketChannel
- ServerSocketChannel 是一个基于通道的 socket 监听器。它同我们所熟悉的 java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞 模式下运行。
- 由于 ServerSocketChannel 没有 bind()方法,因此有必要取出对等的 socket 并使用 它来绑定到一个端口以开始监听连接。我们也是使用对等 ServerSocket 的 API 来根 据需要设置其他的 socket 选项。
- ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象, SocketChannel 可以在非阻塞模式下运行。
1. 如何使用
public static void main(String[] args) throws Exception {
//端口号
int port = 8080;
//把数据放到一个buffer缓冲区
ByteBuffer buf = ByteBuffer.wrap("hello jerry".getBytes());
//ServerSocketChannel
//打开服务器套接字通道
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(port));
//设置非阻塞模式
ssc.configureBlocking(false);
while (true){
SocketChannel sc = ssc.accept();
//监听是否有新连接接入
if(sc == null){
System.out.println("Waiting fro connections");
Thread.sleep(2000);
}else {
System.out.println("Incoming connection from:"+sc.socket().getRemoteSocketAddress());
buf.rewind();//指针0
sc.write(buf);
sc.close();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-euXcbhiC-1669130362009)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211003140446514.png)]
- 打开ServerSocketChannel
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
-
关闭ServerSocketChannel
通过调用
ServerSocketChannel.close() 方法来关闭 ServerSocketChannel.
-
监听新的连接
ServerSocketChannel 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。 因此,需要检查返回的 SocketChannel 是否是 null.如:
while (true){
SocketChannel sc = ssc.accept();
//监听是否有新连接接入
if(sc == null){
System.out.println("Waiting fro connections");
Thread.sleep(2000);
}else {
}
}
2.2.2 SocketChannel
- Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道。
- SocketChannel 主要用途用来处理网络 I/O 的通道
- SocketChannel 是基于 TCP 连接传输
- SocketChannel 实现了可选择通道,可以被多路复用的
- SocketChannel 是用来连接 Socket 套接字
1. 如何使用
- 创建 SocketChannel
SocketChannel socketChannel = SocketChannel.open(new
InetSocketAddress("www.baidu.com", 80));
//或者
SocketChannel socketChanne2 = SocketChannel.open();
socketChanne2.connect(new InetSocketAddress("www.baidu.com", 80));
- 二者区别:直接使用有参 open api 或者使用无参 open api,但是在无参 open 只是创建了一个 SocketChannel 对象,并没有进行实质的 tcp 连接。
- 连接校验
socketChannel.isOpen(); // 测试 SocketChannel 是否为 open 状态
socketChannel.isConnected(); //测试 SocketChannel 是否已经被连接
socketChannel.isConnectionPending(); //测试 SocketChannel 是否正在进行
连接
socketChannel.finishConnect(); //校验正在进行套接字连接的 SocketChannel
是否已经完成连接
- 读写模式
socketChannel.configureBlocking(false);
- 读写
SocketChannel socketChannel = SocketChannel.open(
new InetSocketAddress("www.baidu.com", 80));
ByteBuffer byteBuffer = ByteBuffer.allocate(16);
socketChannel.read(byteBuffer);
socketChannel.close();
System.out.println("read over");
public static void main(String[] args) throws Exception {
SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress("www.baidu.com",80));
//设置阻塞和非阻塞
socketChannel.configureBlocking(false);//非阻塞 //阻塞的话会执行不了下面的代码,阻塞状态
ByteBuffer buf = ByteBuffer.allocate(16);
socketChannel.read(buf);
socketChannel.close();;
System.out.println("readOver");
}
2.2.3 DataGramChannel
- DatagramChannel 则模拟包 导向的无连接协议(如 UDP/IP)
- DatagramChannel 是无连接的,每个数据报 (datagram)都是一个自包含的实体
1. 如何使用
- 打开DatagramChannel
DatagramChannel server = DatagramChannel.open();
server.socket().bind(new InetSocketAddress(10086))
- 接收数据
- 通过receive()接收UDP包
ByteBuffer receiveBuffer = ByteBuffer.allocate(64);
receiveBuffer.clear();
SocketAddress receiveAddr = server.receive(receiveBuffer);
//SocketAddress 可以获得发包的 ip、端口等信息,用 toString 查看,格式如下/127.0.0.1:57126
- 发送数据
- 通过send()发送UDP包
DatagramChannel server = DatagramChannel.open();
ByteBuffer sendBuffer = ByteBuffer.wrap("client send".getBytes());
server.send(sendBuffer, new InetSocketAddress("127.0.0.1",10086));
- 连接
- UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收 发送数据包
client.connect(new InetSocketAddress("127.0.0.1",10086));
int readSize= client.read(sendBuffer);
server.write(sendBuffer);
//连接read和write
@Test
public void testConnect() throws Exception{
DatagramChannel connChannel = DatagramChannel.open();
//绑定
connChannel.bind(new InetSocketAddress(9999));
//连接
connChannel.connect(new InetSocketAddress("127.0.0.1",9999));
//write方法
connChannel.write(ByteBuffer.wrap("发送:jerry".getBytes("UTf-8")));
//buffer
ByteBuffer readBuffer = ByteBuffer.allocate(1024);
while (true){
readBuffer.clear();
connChannel.read(readBuffer);
readBuffer.flip();
System.out.println(Charset.forName("UTF-8").decode(readBuffer));
}
}
三、Buffer
1. 定义
- 用于和NIO通道进行交互。数据是从通道读入缓冲区,从缓冲区写入到通道中
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CmwAW3Vi-1669130362010)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211003145627582.png)]
- 理解缓冲区
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装
成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。缓冲区实际上是
一个容器对象,更直接的说,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲
区处理的。在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到
缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。而在面向流 I/O
系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-3PhfzpQw-1669130362016)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211003145713660.png)]
2. 基本用法
1. 步骤
- 写入数据到buffer
- 调用file方法
- 从buffer中读取数据
- 调用clear()方法或者compact()方法
当向 buffer 写入数据时,buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip()方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 buffer 的所有数据。一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有 两种方式能清空缓冲区:调用 clear()或 compact()方法。clear()方法会清空整个缓冲 区。compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起 始处,新写入的数据将放到缓冲区未读数据的后面。
- 读数据
@Test
public void bufferRead() throws Exception{
RandomAccessFile aFile = new RandomAccessFile("E:\\学习笔记包\\尚硅谷NIO\\src\\NIO_01Channels\\aa.txt","rw");
FileChannel fileChannel = aFile.getChannel();
//创建buffer,大小
ByteBuffer buf = ByteBuffer.allocate(1024);
//读
int bytesRead = fileChannel.read(buf);
while (bytesRead != -1){
//read模式
buf.flip();
while (buf.hasRemaining()){
System.out.println((char)buf.get());
}
buf.clear();
bytesRead = fileChannel.read(buf);
}
aFile.close();
}
- 写数据
@Test
public void write() throws Exception{
IntBuffer buf = IntBuffer.allocate(8);
//buffer放
for (int i = 0; i < buf.capacity(); i++) {
int j = 2*(i+1);
buf.put(j);
}
//重置缓冲区
buf.flip();
//获取
while (buf.hasRemaining()){
int value = buf.get();
System.out.println(value);
}
}
3. 工作原理
3.1 常用属性
- capacity
- 作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”.你只能往里写 capacity 个 byte、long,char 等类型。一旦 Buffer 满了,需要将其清空(通过读数 据或者清除数据)才能继续写数据往里写数据。
- positon
- 1)写数据到 Buffer 中时,position 表示写入数据的当前位置,position 的初始值为 0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入 数据的 Buffer 单元。position 最大可为 capacity – 1(因为 position 的初始值为 0).
- 2)读数据到 Buffer 中时,position 表示读入数据的当前位置,如 position=2 时表 示已开始读入了 3 个 byte,或从第 3 个 byte 开始读取。通过 ByteBuffer.flip()切换 到读模式时 position 会被重置为 0,当 Buffer 从 position 读入数据后,position 会 下移到下一个可读入的数据 Buffer 单元。
- limit
- 1)写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于 Buffer 的 capacity。
- 2)读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到 之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是 position)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PsOtXCpj-1669130362019)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211004103452699.png)]
4. 常用类型
ByteBuffer
MappedByteBuffer
CharBuffer DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
5. Buffer分配和写/读数据方法解读
5.1 Buffer分配
-
要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate 方 法。
-
ByteBuffer buf = ByteBuffer.allocate(48);
-
5.2 写数据到Buffer的两种方法
5.2.1 方式一:从Channel写到buffer的例子
int bytesRead=inChannel.read(buf);//读到buffer
5.2.2 方式二:通过put方法写buffer的例子
buf.put(127);
5.3 file()方法
- flip 方法将 Buffer 从写模式切换到读模式。调用 flip()方法会将 position 设回 0,并 将 limit 设置成之前 position 的值。换句话说,position 现在用于标记读的位置, limit 表示之前写进了多少个 byte、char 等 (现在能读取多少个 byte、char 等)。
5.4 从buffer中读取数据的两种方式
5.4.1 方式一:从 Buffer 读取数据到 Channel 的例子
int bytesWritten = inChannel.write(buf)
5.4.2 方式二:使用 get()方法从 Buffer 中读取数据的例子
byte aByte = buf.get();
5.5 Buffer的常用方法
方法名 | 说明 |
---|---|
rewind() | Buffer.rewind()将 position 设回 0,所以你可以重读 Buffer 中的所有数据。limit 保 持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)。 |
clear() | position 将被设回 0,limit 被设置成 capacity 的值。换 句话说,Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从 哪里开始往 Buffer 里写数据。 |
compact() | compact()方法将所有未读的数据拷贝到 Buffer 起始处。然后将 position 设到最后一 个未读元素正后面。limit 属性依然像 clear()方法一样,设置成 capacity。现在 Buffer 准备好写数据了,但是不会覆盖未读的数据。 |
mark() | 可以标记 Buffer 中的一个特定 position |
reset() | 可以通过调用 Buffer.reset()方法恢复到这个 position |
6. 缓冲区操作
6.1 缓冲区分片
- 定义:在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象 来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的 缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当 于是现有缓冲区的一个视图窗口。调用 slice()方法可以创建一个子缓冲区。
@Test
public void sliceBuf(){
ByteBuffer buf = ByteBuffer.allocate(10);
for (int i = 0; i < buf.capacity(); i++) {
buf.put((byte)i);
}
//创建子缓冲区
buf.position(3);
buf.limit(7);
//创建子缓冲区
ByteBuffer slice = buf.slice();
//改变子缓冲区内容
for (int i = 0; i < slice.capacity(); i++) {
byte b = slice.get(i);
b*=10;
slice.put(i,b);
}
//回到原位
buf.position(0);
buf.limit(buf.capacity());
while (buf.remaining() > 0){
System.out.print(buf.get()+",");
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2EyOba2Y-1669130362021)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211004105208645.png)]
6.2 只读缓冲区
- 只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲 区的 asReadOnlyBuffer()方法,将任何常规缓冲区转 换为只读缓冲区,这个方法返 回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。 如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化:
@Test
public void asReadOnlyBuf(){
ByteBuffer buf = ByteBuffer.allocate(10);
for (int i = 0; i < buf.capacity(); i++) {
byte b = buf.get(i);
b = (byte) (i * 10);
buf.put(i,b);
}
//创建制度缓冲区
ByteBuffer asReadOnlyBuffer = buf.asReadOnlyBuffer();
asReadOnlyBuffer.position(0);
asReadOnlyBuffer.limit(buf.limit());
while (asReadOnlyBuffer.remaining() > 0){
System.out.println(asReadOnlyBuffer.get());
}
}
- 如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException 异常。只读缓冲 区对于保护数据很有用。在将缓冲区传递给某个 对象的方法时,无法知道这个方法是 否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。只 可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。
6.3 直接缓冲区
-
直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK 文档 中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机 I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后), 尝试避免将缓冲区的内容拷贝到一个中间缓冲区中 或者从一个中间缓冲区中拷贝数 据。要分配直接缓冲区,需要调用 allocateDirect()方法,而不是 allocate()方法,使 用方式与普通缓冲区并无区别。
-
拷贝一个文件
@Test
public void allocateDirectBuf() throws Exception{
String inFile = "E:\\学习笔记包\\尚硅谷NIO\\src\\NIO_01Channels\\aa.txt";
FileInputStream fin = new FileInputStream(inFile);
FileChannel finChannel = fin.getChannel();
String outFile = "E:\\学习笔记包\\尚硅谷NIO\\src\\NIO_01Channels\\dd.txt";
FileOutputStream fou = new FileOutputStream(outFile);
FileChannel fouChannel = fou.getChannel();
//创建直接缓冲区
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while (true){
buffer.clear();
int r = finChannel.read(buffer);
if (r == -1){
break;
}
buffer.flip();
fouChannel.write(buffer);
}
}
6.4 内存映射
- 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通 道的 I/O 快的多。内存映射文件 I/O 是通过使文件中的数据出现为 内存数组的内容来 完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这 样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中。
static private final int start = 0;
static private final int size = 1024;
static public void main(String args[]) throws Exception {
RandomAccessFile raf = new RandomAccessFile("d:\\atguigu\\01.txt",
"rw");
FileChannel fc = raf.getChannel();
MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE,
start, size);
mbb.put(0, (byte) 97);
mbb.put(1023, (byte) 122);
raf.close();
}
四、Selector选择器
1. 定义
- Selector 一般称 为选择器 ,也可以翻译为 多路复用器 。它是 Java NIO 核心组件中的一个
2. Selector与Channel的关系
- 用于检查一个或多个 NIO Channel(通道)的状态是否处于可读、可写。如 此可以实现单线程管理多个 channels,也就是可以管理多个网络链接。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1vWfRXmV-1669130362022)(https://gitee.com/jerrygrj/img/raw/master/img/image-20211005100637487.png)]
3. 优点
- 使用 Selector 的好处在于: 使用更少的线程来就可以来处理通道了, 相比使用多个 线程,避免了线程上下文切换带来的开销。
4. 可选择通道(SelectableChannel)
- (1)不是所有的 Channel 都可以被 Selector 复用的。比方说,FileChannel 就不能 被选择器复用。判断一个 Channel 能被 Selector 复用,有一个前提:判断他是否继 承了一个抽象类 SelectableChannel。如果继承了 SelectableChannel,则可以被复 用,否则不能。 (
- 2)SelectableChannel 类提供了实现通道的可选择性所需要的公共方法。它是所有 支持就绪检查的通道类的父类。所有 socket 通道,都继承了 SelectableChannel 类 都是可选择的,包括从管道(Pipe)对象的中获得的通道。而 FileChannel 类,没有继 承 SelectableChannel,因此是不是可选通道。
- (3)一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。通 道和选择器之间的关系,使用注册的方式完成。SelectableChannel 可以被注册到 Selector 对象上,在注册的时候,需要指定通道的哪些操作,是 Selector 感兴趣的。
5. Channel注册到Selector
- (1)使用 Channel.register(Selector sel,int ops)方法,将一个通道注册到一个 选择器时。第一个参数,指定通道要注册的选择器。第二个参数指定选择器需要查询的通道操作。
- (2)可以供选择器查询的通道操作,从类型来分,包括以下四种:
- 可读 : SelectionKey.OP_READ
- 可写 : SelectionKey.OP_WRITE
- 连接 : SelectionKey.OP_CONNECT
- 接收 : SelectionKey.OP_ACCEPT