Java NIO全称 java non-blocking IO
,是指 JDK
提供的新 API
。从JDK1.4
开始,Java
提供了一系列改进的输入/输出的新特性,被统称为 NIO
(即 New IO),是同步非阻塞的。NIO的设计目标是在处理I/O操作时提供更好的性能和可扩展性。它在BIO
功能的基础上实现了非阻塞的特性,位于java.nio
包下。
通俗理解:
NIO
是可以做到用一个线程来处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或者100个线程来处理。不像之前的阻塞10那样,非得分配10000个。
对于同步、异步、阻塞、非阻塞来说,其实很好理解,这里只做简单的介绍
同步、异步、阻塞和非阻塞是与 I/O
操作相关的概念,用于描述程序中的任务调度和执行方式。
同步(Synchronous):
异步(Asynchronous):
阻塞(Blocking):
非阻塞(Non-blocking):
其实这样说还是很难理解,下面让我们用实例说明:
例子:咖啡馆的服务员
同步与阻塞,异步与非阻塞,很多人都会对这两组概念产生疑惑,都会有些区分不清,这是由于它们之间的确是存在关系的,而且是相辅相成的关系,从某种意义上来说:“同步天生就是阻塞的,异步天生就是非阻塞的”。
但实际上又有点不一样,这是我没有画非阻塞的原因(上面画的异步就可以理解成非阻塞的),其实是相辅相成的关系:
非阻塞操作可以是同步的(例如非阻塞 I/O 操作),也可以是异步的。
异步操作可以是阻塞的(例如在异步操作的结果返回前一直等待),也可以是非阻塞的(因为可以去干别的事)。
有点绕,希望你们可以理解
根据上述情况,IO
总共可被分为四大类:同步阻塞式IO
、同步非阻塞式IO
、异步阻塞式IO
、异步非阻塞式IO
,当然,由于异步执行在一定程度上而言,天生就是非阻塞式的,因此不存在异步阻塞式IO
的说法,也就对应着BIO
(同步阻塞)、NIO
(同步非阻塞)、AIO
(异步非阻塞)。
BIO
是常见的IO
,像我们平时写的接口大多都是BIO
,很好理解,这里不做说明;这里通过烧水来举例说明什么是同步非阻塞,以及异步非阻塞
烧水步骤:打开烧水壶的开关—> 烧水中 —> 水开了
同步非阻塞: 现在想象你是传统的烧水壶,它没有任何功能,当你打开烧水壶的开关后,你可以去做别的事,但你会一直隔一会就来看水好了吗?水有没有开呀?直到最终水开;
解释: 这类似于
NIO
模型,其中你可以发起一个I/O
操作(启动开关),然后继续执行其他任务,定期检查状态以确定操作是否完成。
异步非阻塞:现在想象你的水壶是那种响壶,水开了它就会提醒你水开了;当你打开烧水壶开关后,你可以去做别的任何事,直到水壶响了,你就知道水开了,中间你无需反复去检查水是否开
解释: 这类似于
AIO
模型,其中你发起一个I/O
操作,但不需要定期检查状态。相反,系统会在操作完成时通知你。
NIO
和BIO
是两种不同的I/O模型,它们在处理数据的方式和效率上有所不同。
- 处理方式:
BIO
以流的方式处理数据,而NIO
以块的方式处理数据。这意味着在BIO
中,数据是按流逐个字节读取的,而在NIO
中,数据是按块一次读取多个字节。- 效率:由于块I/O的效率比流
I/O
高很多,因此NIO
的效率通常比BIO
高。这是因为NIO
可以一次性读取多个字节,减少了CPU
和内存的访问次数,提高了数据的处理效率。- 阻塞与非阻塞:
BIO
是阻塞的,而NIO
是非阻塞的。在BIO
中,当一个线程进行I/O
操作时,它会一直等待直到操作完成。这会导致线程被阻塞,无法处理其他任务。而在NIO
中,I/O
操作不会阻塞线程,线程可以继续执行其他任务。- 数据传输:在
BIO
中,数据是从字节流或字符流中读取的。而在NIO
中,数据是从通道(Channel)和缓冲区(Buffer)之间传输的。这意味着在NIO
中,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。- 选择器(Selector):
NIO
使用选择器(Selector
)来监听多个通道的事件,如连接请求、数据到达等。因此,使用单个线程就可以监听多个客户端通道。这提高了程序的效率和并发性。
总的来说,NIO
相对于BIO
的优势在于更高的效率、非阻塞性以及使用选择器监听多个通道的能力。然而,这也增加了编程的复杂性。因此,在实际应用中,需要根据具体需求和场景来选择合适的I/O模型。
三大核心原理包括:通道(Channel)、缓冲区(Buffer)和选择器(Selector)。
通道(Channel):
NIO
中,数据通过通道进行传输,通道是双向的,既可以用于读取数据,也可以用于写入数据。通道可以连接到文件、网络套接字等。缓冲区(Buffer):
NIO
中,数据是从通道读取到缓冲区,或者从缓冲区写入到通道。缓冲区提供了对数据的结构化访问,可以轻松地读取、写入、或者处理数据。本质上是一个可以读写数据的内存块选择器(Selector):
NIO
的多路复用器,它允许单个线程处理多个通道。选择器会不断地轮询注册在其上的通道,如果某个通道有数据可读或者可写,就会通知该通道。这种机制使得一个线程可以有效地管理多个通道,提高了系统的性能和资源利用率。记住这张图,对后面理解
NIO
很有帮助!!!不理解没事,后面会详细说明至于什么是多路复用下面有举例说明
Buffer
是一个可以读写的内存块,是双向通道,既可以读也可以写。Selector
用于监听多个Channel
的事件,例如连接请求、数据到达等。每个线程可以处理多个Channel的连接和事件。Channel
是NIO
中的核心概念之一,用于建立连接并传输数据。每个Channel
都会注册到Selector
上,以便Selector
能够监听该Channel
的事件。
想象一个大型餐厅,其中有许多客人等待就餐。传统的方式是,每个客人都要有一个服务员单独服务,这样每个服务员只能为一个客人服务。但这种方式非常低效,因为当一个服务员忙于服务一个客人时,其他客人只能等待。
现在想象一个改进的餐厅,其中只有一个多路复用器服务员。这个服务员可以在多个桌子之间巡回,为每个客人提供服务。当一个客人需要点菜时,多路复用器服务员会记下客人的需求,然后继续为其他客人服务。当所有的客人都点完菜后,多路复用器服务员会回到第一个客人那里,为他上菜。
在这个例子中,多路复用器服务员就像NIO
中的Selector
。它可以在多个Channel
之间进行选择,当其中一个Channel
有数据可读或可写时,它会通知相应的线程进行操作。这种方式极大地提高了餐厅的效率和服务质量,因为服务员可以同时为多个客人服务,而不是只能为一个客人服务。
缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个容器对象(含数组),该对象提供了一组方法,可以更轻松地使用内存块,缓冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变化情况。Channel
提供从文件、网络读取数据的渠道,但是读取或写入的数据都必须经由 Buffer;
这些缓冲区类都继承自Buffer类,具有一些通用的方法和属性,如position、limit、capacity等。它们的主要区别在于存储的数据类型不同,因此在使用时需要根据具体需求选择合适的缓冲区类。
使用Buffer读写数据一般遵循以下四个步骤:
buffer.clear()
方法或者buffer.compact()
方法清除缓冲区对于Java中缓冲区的定义,首先要明白,当缓冲区被创建出来后,同一时刻只能处于读/写中的一个状态,同一时间内不存在即可读也可写的情况。理解这点后再来看看它的成员变量,重点理解下述三个成员:
pasition
:表示当前操作的索引位置(下一个要读/写数据的下标)。capacity
:表示当前缓冲区的容量大小。limit
:表示当前可允许操作的最大元素位置(不是下标,是正常数字)。
图示:
Java中的缓冲区也被分为了两大类:本地直接内存缓冲区与堆内存缓冲区,前面Buffer
类的所有子实现类xxxBuffer
本质上还是抽象类,每个子抽象类都会有DirectXxxBuffer、HeapXxxBuffer
两个具体实现类,这两者的主要区别在于:创建缓冲区的内存是位于堆空间之内还是之外。
ByteBuffer
类的静态工厂方法来创建,如ByteBuffer.allocate()
。NIO
的ByteBuffer
类的allocateDirect()
方法创建的。优缺点:
堆内存缓冲区在创建和销毁时相对较轻量级,但可能会受到垃圾回收的影响。
而直接内存缓冲区虽然不会受到垃圾回收的影响,但创建和销毁时可能更消耗资源。
MappedByteBuffer是Java NIO(New I/O)中引入的文件内存映射方案,它允许Java程序直接从内存中读取文件内容。通过将整个或部分文件映射到内存,由操作系统来处理加载请求和写入文件,应用只需要和内存打交道,这使得IO操作非常快。
MappedByteBuffe
r的设计使得它能够高效地处理大文件。传统的文件IO操作中,我们需要调用操作系统提供的底层标准IO系统调用函数(如read()、write()),此时调用此函数的进程(在JAVA中即java进程)由当前的用户态切换到内核态,然后OS的内核代码负责将相应的文件数据读取到内核的IO缓冲区,然后再把数据从内核IO缓冲区拷贝到进程的私有地址空间中去,这样便完成了一次IO操作。而通过MappedByteBuffer,我们可以直接从内存中读取文件内容,避免了上述的IO操作过程,从而提高了IO操作的效率。
public class MappedByteBufferTest {
public static void main(String[] args) throws Exception {
RandomAccessFile randomAccessFile = new RandomAccessFile("1.txt", "rw");
//获取对应的通道
FileChannel channel = randomAccessFile.getChannel();
/**
* 参数1: FileChannel.MapMode.READ_WRITE 使用的读写模式
* 参数2: 0 : 可以直接修改的起始位置
* 参数3: 5: 是映射到内存的大小(不是索引位置) ,即将 1.txt 的多少个字节映射到内存
* 可以直接修改的范围就是 0-5
* 实际类型 DirectByteBuffer
*/
MappedByteBuffer mappedByteBuffer = channel.map(FileChannel.MapMode.READ_WRITE, 0, 5);
mappedByteBuffer.put(0, (byte) 'H');
mappedByteBuffer.put(3, (byte) '9');
mappedByteBuffer.put(5, (byte) 'Y');//IndexOutOfBoundsException
randomAccessFile.close();
System.out.println("修改成功~~");
}
}
NIO中的通道(Channel
)是一个用于数据传输的双向通道,既可以读也可以写。与流(Stream
)相比,通道具有更好的扩展性和灵活性,能够更好地映射底层操作系统的API
。
具体来说,通道和流的主要区别在于通道是双向的,而流只是在一个方向上移动。流必须是
InputStream
或OutputStream
的子类,而通道可以用于读、写或者同时用于读写。另外,通道是全双工的,可以同时进行读写操作,而流只能是单向的。通道可以分为两大类:用于网络读写的
SelectableChannel
和用于文件操作的FileChannel
。SelectableChannel
可以被选择器(Selector)选择,从而实现多路复用。FileChannel
则主要用于文件的读写操作,支持异步读写和文件区域操作等高级功能。通道的使用方式也与流有所不同。从一个通道中读取数据很简单:只需创建一个缓冲区,然后让通道将数据读到这个缓冲区中。写入也相当简单:创建一个缓冲区,用数据填充它,然后让通道用这些数据来执行写入操作。
与流相比,通道的优势在于它们可以更好地映射底层操作系统的
API
,具有更高的效率和更好的扩展性。另外,通道还支持异步读写操作,这使得它们在处理大量并发操作时更加高效。
通道不是打开就是关闭。通道在创建时是开放的,一旦关闭,它就保持关闭状态。一旦通道关闭,任何对其调用I/O操作的尝试都将导致抛出
closechannelexception。
通道是否打开可以通过调用它的isOpen
方法来测试。 一般来说,通道对于多线程访问是安全的,这在扩展和实现该接口的接口和类的规范中有描述。
// NIO包中定义的Channel通道接口
public interface Channel extends Closeable {
// 判断通道是否处于开启状态
public boolean isOpen();
// 关闭通道
public void close() throws IOException;
}
其中常用的
FileChannel
以及ServerSocketChannel
分别位于ReadableByteChannel
和SelectableChannel
下
常用的Channel类有:
FileChannel
:用于读取、写入、映射和操作文件的通道。DatagramChannel
:通过 UDP 读写网络中的数据通道。SocketChannel
:通过 TCP 读写网络中的数据。ServerSocketChannel
:可以监听新进来的 TCP 连接,对每一个新进来的连接都会创建一个 SocketChannel
。ServerSocketChanne 类似 ServerSocket , SocketChannel 类似 Socket
FileChannel类常用方法:
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel
{
// 从Channel 到 中读取数据到 ByteBuffer
public int read(ByteBuffer dst);
//将Channel中的数据“分散”到 ByteBuffer[]
public long read(ByteBuffer[] dsts);
//将ByteBuffer中的数据写入到 Channel
public int write(ByteBuffer src) ;
//将ByteBuffer[] 到 中的数据“聚集”到 Channel
public long write(ByteBuffer[] srcs);
//返回此通道的文件位置
public long position();
//设置此通道的文件位置
public FileChannel position(long p);
//返回此通道的文件的当前大小
public long size() ;
//将此通道的文件截取为给定大小
public FileChannel truncate(long s) ;
//强制将所有对此通道的文件更新写入到存储设备中
public void force(boolean metaData);
}
在案例一、二、三说明!
SelectableChannel其中存在两个重要的子类:分别是ServerSockerChannel和SockerChannel
SocketChannel和ServerSocketChannel的关系:
SocketChannel
是用于客户端的网络通信,它可以通过建立与ServerSocket
的连接来与服务器进行通信。而ServerSocketChannel
则是用于服务器端的网络通信,它可以通过监听新进来的连接请求来接受客户端的连接。在传统的
Socket
编程中,客户端需要建立一个Socket
对象来与服务器建立连接。而在Java NIO
中,客户端可以使用SocketChannel
来代替Socket
进行网络通信。同样地,服务器端也可以使用ServerSocketChannel
来代替ServerSocket
进行网络通信。
SocketChannel
和ServerSocketChannel
之间的主要区别在于它们的使用场景不同。SocketChannel
主要用于客户端,而ServerSocketChannel
主要用于服务器端。但是,它们都提供了异步、高效的网络I/O操作能力,使得客户端和服务器之间的通信更加高效和可靠。
ServerSockerChannel类常用说明:
// 服务端通道抽象类
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel
{
// 构造方法:需要传递一个选择器进行初始化构建
protected ServerSocketChannel(SelectorProvider provider);
// 打开一个ServerSocketChannel通道
public static ServerSocketChannel open() throws IOException;
// 绑定一个IP地址作为服务端
public final ServerSocketChannel bind(SocketAddress local);
// 绑定一个IP并设置并发连接数大小,超出后的连接全部拒绝
public abstract ServerSocketChannel bind(SocketAddress local, int backlog);
// 监听客户端连接的方法(会发生阻塞的方法)
public abstract SocketChannel accept() throws IOException;
// 获取一个ServerSocket对象
public abstract ServerSocket socket();
// .....省略其他方法......
}
ServerSocketChannel
类似 ServerSocket
。主要作用是接受客户端的连接请求,并建立TCP连接。当有客户端尝试连接到服务器时,ServerSocketChannel
可以监听到这个连接请求,并接受该连接。一旦连接建立,ServerSocketChannel
就可以将接收到的数据传递给对应的处理程序进行进一步处理。
可以这么理解ServerSocketChanel本生也是channel,他也需要selector去监听事件发生,有新客户端连接,selector就监听到了该事件
SockerChannel类常用说明:
public abstract class SocketChannel extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel,
GatheringByteChannel, NetworkChannel{
// 打开一个通道
public static SocketChannel open();
// 根据指定的远程地址,打开一个通道
public static SocketChannel open(SocketAddress remote);
// 如果调用open()方法时未给定地址,可以通过该方法连接远程地址
public abstract boolean connect(SocketAddress remote);
// 将当前通道绑定到本地套接字地址上
public abstract SocketChannel bind(SocketAddress local);
// 把当前通道注册到Selector选择器上:
// sel:要注册的选择器、ops:事件类型、att:共享属性。
public final SelectionKey register(Selector sel,int ops,Object att);
// 省略其他......
// 关闭通道
public final void close();
// 向通道中写入数据,数据通过缓冲区的方式传递
public abstract int write(ByteBuffer src);
// 根据给定的起始下标和数量,将缓冲区数组中的数据写入到通道中
public abstract long write(ByteBuffer[] srcs,int offset,int length);
// 向通道中批量写入数据,批量写入一个缓冲区数组
public final long write(ByteBuffer[] srcs);
// 从通道中读取数据(读取的数据放入到dst缓冲区中)
public abstract int read(ByteBuffer dst);
// 根据给定的起始下标和元素数据,在通道中批量读取数据
public abstract long read(ByteBuffer[] dsts,int offset,int length);
// 从通道中批量读取数据,结果放入dits缓冲区数组中
public final long read(ByteBuffer[] dsts);
// 返回当前通道绑定的本地套接字地址
public abstract SocketAddress getLocalAddress();
// 判断目前是否与远程地址建立上了连接关系
public abstract boolean isConnected();
// 判断目前是否与远程地址正在建立连接
public abstract boolean isConnectionPending();
// 获取当前通道连接的远程地址,null代表未连接
public abstract SocketAddress getRemoteAddress();
// 设置阻塞模式,true代表阻塞,false代表非阻塞
public final SelectableChannel configureBlocking(boolean block);
// 判断目前通道是否为打开状态
public final boolean isOpen();
}
SocketChannel
所提供的方法大体分为三类:
看到这里如果还有什么不清楚的话可以先去看看案例一、二、三、四
SelectionKey是Java NIO中的一个抽象类,表示selectableChannel在Selector中注册的标识。每个Channel向Selector注册时,都会创建一个SelectionKey,将Channel与Selector建立了关系,并维护了channel事件。
SelectionKey有四个操作类型:OP_READ(当操作系统读缓冲区有数据可读时)、OP_WRITE(当操作系统写缓冲区有数据可写时)、OP_CONNECT(当连接被成功建立时)、OP_ACCEPT(当新连接被接受时)。
在编程时,通过SelectionKey可以获得通道的IO事件类型,比方说SelectionKey.OP_READ;还可以获得发生IO事件所在的通道;
public abstract class SelectionKey {
//得到与之关联的通道
public abstract SelectableChannel channel();
//得到与之关联的Selector 对象
public abstract Selector selector();
//设置或改变监听事件
public abstract SelectionKey interestOps(int ops);
///是否可以读
public final boolean isReadable();
// 是否可以写
public final boolean isWritable();
// 是否接受新的连接
public final boolean isAcceptable();
//得到与之关联的共享数据(Buffer)
public final Object attachment();
}
Selector是一个选择器,它用于检测一个或者多个NIO
通道的状态是否处于可读、可写。Selector
的使用可以实现单线程管理多个Channel,也就是可以管理多个网络链接。
使用Selector的好处在于,只需要更少的线程就可以来处理通道,避免了线程上下文切换带来的开销。但是,不是所有的Channel都可以被Selector
复用,只有继承了SelectableChannel
的Channel才能被Selector复用。
Selector是非阻塞IO的核心。
要使用
Selector
,首先需要创建一个Selector
对象,然后通过Channel的register()
方法将Channel
注册到Selector上,注册时需要指定监听的事件类型。注册成功后,就可以通过Selector的select()
方法来检测是否有事件发生,如果有事件发生,就可以通过Selector的selectedKeys()
方法获取到发生事件的所有Channel,然后进行处理。
通道一共支持4
中事件:
SelectionKey.OP_READ/1
:读取就绪事件,通道内的数据已就绪可被读取。
SelectionKey.OP_WRITE/4
:写入就绪事件,一个通道正在等待数据写入。
SelectionKey.OP_CONNECT/8
:连接就绪事件,通道已成功连接到服务端。
SelectionKey.OP_ACCEPT/16
:接收就绪事件,服务端通道已准备好接收新的连接。
当一个通道注册时,会为其绑定对应的事件,当该通道触发了一个事件,就代表着该事件已经准备就绪,可以被线程操作了。当然,如果要为一条通道绑定多个事件,那可通过位或操作符拼接:
public abstract class Selector implements Closeable {
// 创建一个选择器
public static Selector open() throws IOException;
// 判断一个选择器是否已打开
public abstract boolean isOpen();
// 获取创建当前选择器的生产者对象
public abstract SelectorProvider provider();
// 获取所有注册在当前选择的通道连接
public abstract Set<SelectionKey> keys();
// 获取所有数据已准备就绪的通道连接
public abstract Set<SelectionKey> selectedKeys();
// 非阻塞式获取就绪的通道,如若没有就绪的通道则会立即返回
public abstract int selectNow() throws IOException;
// 在指定时间内,阻塞获取已注册的通道中准备就绪的通道数量
public abstract int select(long timeout) throws IOException;
// 获取已注册的通道中准备就绪的通道数量(阻塞式)
public abstract int select() throws IOException;
// 唤醒调用Selector.select()方法阻塞后的线程
public abstract Selector wakeup();
// 关闭创建的选择器(不会关闭通道)
public abstract void close() throws IOException;
}
需求:使用前面学习后的 **ByteBuffer(缓冲)**和 FileChannel(通道), 将数据写入到 data.txt 中
public class NIOFileChannel01 {
public static void main(String[] args) throws Exception{
String str = "hello,邱俊杰";
//创建一个输出流->channel
FileOutputStream fileOutputStream = new FileOutputStream("d:\\data.txt");
//通过 fileOutputStream 获取 对应的 FileChannel
//这个 fileChannel 真实 类型是 FileChannelImpl
FileChannel fileChannel = fileOutputStream.getChannel();
//创建一个缓冲区 ByteBuffer
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
//将 str 放入 byteBuffer
byteBuffer.put(str.getBytes());
//对byteBuffer 进行flip
byteBuffer.flip();
//将byteBuffer 数据写入到 fileChannel
fileChannel.write(byteBuffer);
fileOutputStream.close();
}
}
使用前面学习后的ByteBuffer(缓存)和FileChannel(通道),将file01.txt中的数据读入到程序,并显示在控制台屏幕
public class NIOFileChannel02 {
public static void main(String[] args) throws Exception {
//创建文件的输入流
File file = new File("d:\\file01.txt");
FileInputStream fileInputStream = new FileInputStream(file);
//通过fileInputStream 获取对应的FileChannel -> 实际类型 FileChannelImpl
FileChannel fileChannel = fileInputStream.getChannel();
//创建缓冲区(创建文件一样大小的Buffer)
ByteBuffer byteBuffer = ByteBuffer.allocate((int) file.length());
//将 通道的数据读入到Buffer
fileChannel.read(byteBuffer);
//将byteBuffer 的 字节数据 转成String
System.out.println(new String(byteBuffer.array()));
fileInputStream.close();
}
}
使用 FileChannel(通道)和 方法 read
,write
,完成文件的拷贝(将1.txt中的数据拷贝到2.txt)
public class NIOFileChannel03 {
public static void main(String[] args) throws Exception {
FileInputStream fileInputStream = new FileInputStream("1.txt");
FileChannel fileChannel01 = fileInputStream.getChannel();
FileOutputStream fileOutputStream = new FileOutputStream("2.txt");
FileChannel fileChannel02 = fileOutputStream.getChannel();
ByteBuffer byteBuffer = ByteBuffer.allocate(512);
//循环读取
while (true) {
//这里有一个重要的操作,清空buffer
byteBuffer.clear();
int read = fileChannel01.read(byteBuffer);
System.out.println("read =" + read);
if(read == -1) { //表示读完
break;
}
//将buffer 中的数据写入到 fileChannel02 -- 2.txt
byteBuffer.flip();
fileChannel02.write(byteBuffer);
}
//关闭相关的流
fileInputStream.close();
fileOutputStream.close();
}
}
使用FileChannel(通道)和方法transferFrom
,完成文件的拷贝
public class NIOFileChannel04 {
public static void main(String[] args) throws Exception {
//创建相关流
FileInputStream fileInputStream = new FileInputStream("d:\\a.jpg");
FileOutputStream fileOutputStream = new FileOutputStream("d:\\a2.jpg");
//获取各个流对应的filechannel
FileChannel sourceCh = fileInputStream.getChannel();
FileChannel destCh = fileOutputStream.getChannel();
//使用transferForm完成拷贝
destCh.transferFrom(sourceCh,0,sourceCh.size());
//关闭相关通道和流
sourceCh.close();
destCh.close();
fileInputStream.close();
fileOutputStream.close();
}
}
使用FileChannel(通道)和方法transferTo
,完成文件的复制
public class NIOFileChannel05 {
public static void main(String[] args) throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("E:\\test\\Aurora-4k.jpg");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("E:\\test\\Aurora-4knew4.jpg");
FileChannel osChannel = fos.getChannel();
// 3、复制
isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
isChannel.close();
osChannel.close();
}
}
Scattering:将数据写入到buffer时,可以采用buffer数组,依次写入 [分散]
Gathering:从buffer读取数据时,可以采用buffer数组,依次读
public class ScatteringAndGatheringTest {
public static void main(String[] args) throws Exception {
//使用 ServerSocketChannel 和 SocketChannel 网络
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
InetSocketAddress inetSocketAddress = new InetSocketAddress(7000);
//绑定端口到socket ,并启动
serverSocketChannel.socket().bind(inetSocketAddress);
//创建buffer数组
ByteBuffer[] byteBuffers = new ByteBuffer[2];
byteBuffers[0] = ByteBuffer.allocate(5);
byteBuffers[1] = ByteBuffer.allocate(3);
//等客户端连接(telnet)
SocketChannel socketChannel = serverSocketChannel.accept();
int messageLength = 8; //假定从客户端接收8个字节
//循环的读取
while (true) {
int byteRead = 0;
while (byteRead < messageLength ) {
long l = socketChannel.read(byteBuffers);
byteRead += l; //累计读取的字节数
System.out.println("byteRead=" + byteRead);
//使用流打印, 看看当前的这个buffer的position 和 limit
Arrays.asList(byteBuffers).stream().map(buffer -> "postion=" + buffer.position() + ", limit=" + buffer.limit()).forEach(System.out::println);
}
//将所有的buffer进行flip
Arrays.asList(byteBuffers).forEach(buffer -> buffer.flip());
//将数据读出显示到客户端
long byteWirte = 0;
while (byteWirte < messageLength) {
long l = socketChannel.write(byteBuffers); //
byteWirte += l;
}
//将所有的buffer 进行clear
Arrays.asList(byteBuffers).forEach(buffer-> {
buffer.clear();
});
System.out.println("byteRead:=" + byteRead + " byteWrite=" + byteWirte + ", messagelength" + messageLength);
}
}
}
需求:服务端接收客户端的连接请求,并接收多个客户端发送过来的事件。
Server端代码实现:
public class NIOServer {
public static void main(String[] args) throws Exception{
//创建ServerSocketChannel -> ServerSocket
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
//得到一个Selecor对象
Selector selector = Selector.open();
//绑定一个端口6666, 在服务器端监听
serverSocketChannel.socket().bind(new InetSocketAddress(6666));
//设置为非阻塞
serverSocketChannel.configureBlocking(false);
//把 serverSocketChannel 注册到 selector 关心 事件为 OP_ACCEPT
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("注册后的selectionkey 数量=" + selector.keys().size()); // 1
//循环等待客户端连接
while (true) {
//这里我们等待1秒,如果没有事件发生, 返回
if(selector.select(1000) == 0) { //没有事件发生
System.out.println("服务器等待了1秒,无连接");
continue;
}
//如果返回的>0, 就获取到相关的 selectionKey集合
//1.如果返回的>0, 表示已经获取到关注的事件
//2. selector.selectedKeys() 返回关注事件的集合
// 通过 selectionKeys 反向获取通道
Set<SelectionKey> selectionKeys = selector.selectedKeys();
System.out.println("selectionKeys 数量 = " + selectionKeys.size());
//遍历 Set, 使用迭代器遍历
Iterator<SelectionKey> keyIterator = selectionKeys.iterator();
while (keyIterator.hasNext()) {
//获取到SelectionKey
SelectionKey key = keyIterator.next();
//根据key 对应的通道发生的事件做相应处理
if(key.isAcceptable()) { //如果是 OP_ACCEPT, 有新的客户端连接
//该该客户端生成一个 SocketChannel
SocketChannel socketChannel = serverSocketChannel.accept();
System.out.println("客户端连接成功 生成了一个 socketChannel " + socketChannel.hashCode());
//将 SocketChannel 设置为非阻塞
socketChannel.configureBlocking(false);
//将socketChannel 注册到selector, 关注事件为 OP_READ, 同时给socketChannel
//关联一个Buffer
socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024));
System.out.println("客户端连接后 ,注册的selectionkey 数量=" + selector.keys().size()); //2,3,4..
}
if(key.isReadable()) { //发生 OP_READ
//通过key 反向获取到对应channel
SocketChannel channel = (SocketChannel)key.channel();
//获取到该channel关联的buffer
ByteBuffer buffer = (ByteBuffer)key.attachment();
channel.read(buffer);
System.out.println("form 客户端 " + new String(buffer.array()));
}
//手动从集合中移动当前的selectionKey, 防止重复操作
keyIterator.remove();
}
}
}
}
Client端代码实现:
public class NIOClient {
public static void main(String[] args) throws Exception{
//得到一个网络通道
SocketChannel socketChannel = SocketChannel.open();
//设置非阻塞
socketChannel.configureBlocking(false);
//提供服务器端的ip 和 端口
InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 6666);
//连接服务器
if (!socketChannel.connect(inetSocketAddress)) {
while (!socketChannel.finishConnect()) {
System.out.println("因为连接需要时间,客户端不会阻塞,可以做其它工作..");
}
}
//...如果连接成功,就发送数据
String str = "hello, 邱俊杰~";
//将字节数组包装到缓冲区中
ByteBuffer buffer = ByteBuffer.wrap(str.getBytes());
//发送数据,将 buffer 数据写入 channel
socketChannel.write(buffer);
System.in.read();
}
}
现在返回来看以下这个图的话,相信大家会有一个新的理解了
对上图说明:
ServerSocketChannel
得到SocketChannel
Selector
进行监听 select
方法,返回有事件发生的通道的个数.socketChannel
注册到Selector
上,register(Selector sel,int ops)
,一个selector
上可以注册多个SocketChannel
SelectionKey
,会和该Selector
关联(集合)SelectionKey
(有事件发生)SelectionKey
反向获取 SocketChannel
,方法 channel()
channel
,完成业务处理要弄清楚什么是零拷贝,首先得理解两个重要的概念,即:用户态与内核态
那什么是用户态和内核态呢?想看图片!
操作系统中的用户态(User Mode
)和内核态(Kernel Mode
)是两种不同的运行模式,涉及到程序执行时与操作系统内核的交互方式。这两种模式有不同的权限和特权级别。
切换模式:
在操作系统中,程序从用户态切换到内核态需要通过系统调用(
System Call
)或者中断(Interrupt
)等方式。这是为了防止应用程序滥用对系统资源的访问权限。当应用程序需要进行一些特权操作时,例如文件读写、网络通信等,它会通过系统调用进入内核态执行相应的操作,然后再返回用户态。
为什么OS要区分用户态和内核态
区分用户态和内核态的主要目的是保护操作系统程序,并确保计算机系统的运行安全。在多道程序环境下,为了保障计算机系统的运行安全,将计算机系统中的指令分为两类:特权指令和非特权指令。能引起系统损害的机器指令称为特权指令,否则称为非特权指令。操作系统模式(内核态)下可执行特权指令和非特权指令,用户模式(用户态)下只能执行非特权指令。当CPU处于用户态时只能执行非特权指令,并且只能访问当前运行进程(运行的用户程序)的地址空间,这样才能有效地保护操作系统内核及内存中其他用户程序不受该运行进程(程序)的侵害。
用户态和内核态的切换是由处理机状态寄存器中的上下文信息控制的。当处理机从一个状态切换到另一个状态时,它会保存当前的上下文信息,以便在需要时可以恢复到之前的状态。这种机制使得处理机可以在不同状态下执行不同的任务,从而提高了系统的效率和可靠性。总之,处理机区分内核态和用户态是为了保护系统的安全性和稳定性,这种机制可以提高系统的效率和可靠性。(摘取《操作系统概念》)
举例说明:
其实学过网络编程的应该知道
Socket
,一般来说,或者我们听到的,都是Socket
协议,或者Socket
连接;但Socket 其实并不是一个协议,而是为了方便使用
TCP
或UDP
而抽象出来的一层,是位于应用层和传输控制层之间的一组接口。 Socket本身并不是一个协议,它工作在OSI
模型会话层,是一个套接字,TCP/IP网络的API,是为了方便大家直接使用。更底层协议而存在的一个抽象层。Socket其实就是一个门面模式(用户态),它把复杂的**TCP/IP协议族(内核态)**隐藏在Socket接口后面,对用户来说,一组简单的接口就是全部,让Socket去组织数据,以符合指定的协议。
而WebSocket
则是一个典型的应用层协议。
这样可能还是不好理解,我们重新用生活中的例子说明:
- 用户态(驾驶者):
- 当你驾驶汽车时,你处于用户态。你能够直接操控方向盘、刹车、油门等汽车的用户接口。这就好比用户态的应用程序,它能够执行各种任务,但受到一定的限制。
- 内核态(汽车引擎控制系统):
- 与此同时,汽车的引擎控制系统工作在内核态。这个系统负责管理引擎的运行、燃油供应、排放控制等核心功能。这些功能对于汽车的正常运行至关重要,就像操作系统内核管理系统的核心资源一样。
- 用户态和内核态的切换:
- 当你需要进行某些高级操作,比如调整引擎映射、查看车辆诊断信息等时,你可能需要将汽车引擎控制系统切换到用户模式,这就好比进行系统调用。但大多数时间,你只需在用户态进行驾驶,引擎控制系统在内核态默默地处理所有必要的事务。
- 保护核心功能:
- 想象一下,如果任何人都能够直接干预引擎的内部工作,那么汽车的安全性和可靠性就会大大降低。引擎控制系统运行在内核态,提供了对核心功能的保护,防止不懂引擎工作原理的人随意操作。
到这里,我们应该对用户态和内核态有了一个大概的了解!!那回到我们的主题;什么是零拷贝?
零拷贝是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。具体来说,在数据传输过程中,源节点到目的节点之间需要将数据从一个存储区复制到另一个存储区,而这个过程会产生一些额外的CPU指令和上下文切换,从而导致一定的性能损失。而零拷贝技术可以避免这个过程,从而提高了数据传输的效率。
总结起来就是一句话:零拷贝从操作系统角度,就是没有cpu拷贝
在Java 程序中,常用的零拷贝有 mmap
(内存映射)和 sendFile
。那么,他们在 OS里,到底是怎么样的一个的设计?
Java传统IO和网络编程的一段代码:
public class BIOModel {
public static void main(String[] args) throws Exception{
File file = new File("1.txt");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
byte[] bytes = new byte[(int) file.length()];
raf.read(bytes);
Socket socket = new ServerSocket(8080).accept();
socket.getOutputStream().write(bytes);
}
}
上面代码的图解:
DMA:
direct memory access
直接内存拷贝(不使用 CPU)
在传统IO中一共经过了4次切换以及4次拷贝,具体过程:
用户进程调用 read 方法,向操作系统发出 I/O 请求(上下文从用户态转向内核态),请求读取数据到自己的内存缓冲区中,进程进入阻塞状态;
操作系统收到请求后,进一步将 I/O 请求发送 DMA,然后让 CPU 执行其他任务;
DMA 进一步将 I/O 请求发送给磁盘;
磁盘收到 DMA 的 I/O 请求,把数据从磁盘读取到磁盘控制器的缓冲区中,当磁盘控制器的缓冲区被读满后,向 DMA 发起中断信号,告知自己缓冲区已满;
DMA 收到磁盘的信号,将磁盘控制器缓冲区中的数据拷贝到内核缓冲区中,此时不占用 CPU,CPU 可以执行其他任务;
当 DMA 读取了足够多的数据,就会发送中断信号给 CPU;
CPU 收到 DMA 的信号,知道数据已经准备好,于是将数据从内核拷贝到用户空间,系统调用返回(上下文从内核态转为用户态);
用户进程调用 write方法,向操作系统发出 I/O 请求(上下文从用户态转为内核态),CPU将读缓冲区中数据拷贝到socket缓冲区。
DMA控制器把数据从socket缓冲区拷贝到网卡,(上下文从内核态切换回用户态),write()返回。
这里DMA拷贝2次,CPU拷贝2次,所谓的零拷贝,也可以理解成没有CPU参与的拷贝
mmap通过内存映射,将文件映射到内核缓冲区,同时,用户空间可以共享内核空间的数据。这样,在进行网络传输时,就可以减少内核空间到用户空间的拷贝次数。如下图:
mmap方式的零拷贝经过了4次上下文切换和3次数据拷贝。
具体流程如下:
mmap()
方法向操作系统发起调用,上下文从用户态转向内核态。write()
方法发起调用,上下文从用户态转为内核态,CPU将读缓冲区中数据拷贝到socket缓冲区。相比mmap,传统IO多了一次CPU拷贝。
在传统IO中,数据首先被读取到内核缓冲区,然后再从内核缓冲区复制到用户程序缓冲区。而使用mmap技术,数据可以直接从文件映射到内存中,用户程序可以直接对内存进行读写操作,避免了额外的数据复制。因此,相比于传统的IO,mmap方式少了一次CPU拷贝。
Linux 2.1 版本 提供了sendFile
函数,其基本原理如下:数据根本不经过用户态,直接从内核缓冲区进入到Socket Buffer,同时,由于和用户态完全无关,就减少了一次上下文切换
整个过程发生了2次用户态和内核态的上下文切换和3次拷贝,具体流程如下:
sendfile()
方法向操作系统发起调用,上下文从用户态转向内核态sendfile
调用返回sendfile
方法IO数据对用户空间完全不可见,所以只能适用于完全不需要用户空间处理的情况,比如静态文件服务器。
Linux2.4内核版本之后对sendfile
做了进一步优化,通过引入新的硬件支持,这个方式叫做DMA Scatter/Gather 分散/收集功能。
整个过程发生了2次用户态和内核态的上下文切换和2次拷贝,其中更重要的是完全没有CPU拷贝,具体流程如下:
sendfile()
方法向操作系统发起调用,上下文从用户态转向内核态sendfile()
调用返回,上下文从内核态切换回用户态真正意义上的零拷贝!!!
其实在上面的案例四和案例五中,我们已经用到了零拷贝;也就是NIO中的transferTo
方法和transferFrom
方法:
public class NIOFileChannel05 {
public static void main(String[] args) throws Exception {
// 1、字节输入管道
FileInputStream is = new FileInputStream("E:\\test\\Aurora-4k.jpg");
FileChannel isChannel = is.getChannel();
// 2、字节输出流管道
FileOutputStream fos = new FileOutputStream("E:\\test\\Aurora-4knew4.jpg");
FileChannel osChannel = fos.getChannel();
// 3、复制
isChannel.transferTo(isChannel.position() , isChannel.size() , osChannel);
isChannel.close();
osChannel.close();
}
}
注意:
transferTo
只能发送8m , 就需要分段传输文件如果 Linux 系统支持 sendfile()
系统调用,那么 transferTo()
实际上最后就会使用到 sendfile()
系统调用函数。
NIO(Non-blocking I/O,非阻塞I/O)是一种基于Channel
和Buffer
的I/O模型,它支持异步和多路复用。在NIO中,Channel是用于进行I/O操作的通道,Buffer是用于存储数据的容器。NIO通过Selector来监听Channel的事件,从而实现非阻塞的I/O操作。
NIO的核心是Channel和Buffer,它们之间的关系是Channel
通过Buffer
进行数据的读写操作。在NIO中,所有的I/O操作都是异步的,这意味着不需要等待操作完成就可以继续执行其他任务。当操作完成后,会通知相关线程进行后续处理。
NIO的优点包括:
Selector
监听多个Channel
的事件,可以同时处理多个I/O操作,提高效率。NIO的实战场景包括:
[尚硅谷Netty教程](027_尚硅谷_SelectionKey API_哔哩哔哩_bilibili)
零拷贝