NIO是什么
NIO (New lO)也有人称之为java non-blocking lO是从Java 1.4版本开始引入的一个新的IO API,可以替代标准的Java lO API。NIO与原来的IO有同样的作用和目的,但是使用的方式完全不同,NIO支持面向缓冲区的、基于通道的IO操作。NIO将以更加高效的方式进行文件的读写操作。NIO可以理解为非阻塞IO,传统的IO的read和write只能阻塞执行,线程在读写IO期间不能干其他事情,比如调用socket.read()时,如果服务器一直没有数据传输过来,线程就一直阻塞,而NIO中可以配置socket为非阻塞模式。
- NIO相关类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写。
- NIO有三大核心部分:Channel(通道),Buffer(缓冲区), Selector(选择器)
- Java NlO的非阻塞模式,使一个线程从某通道发送请求或者读取数据,但是它仅能得到目前可用的数据,如果目前没有数据可用时,就什么都不会获取,而不是保持线程阻塞,所以直至数据变的可以读取之前,该线程可以继续做其他的事情。非阻塞写也是如此,一个线程请求写入一些数据到某通道,但不需要等待它完全写入,这个线程同时可以去做别的事情。
- 通俗理解:NIO是可以做到用一个线程来处理多个操作的。假设有1000个请求过来,根据实际情况,可以分配20或者80个线程来处理。不像之前的阻塞IO那样,非得分配1000个。
NIO与BIO的区别
-
BIO以流的方式处理数据,而NIO以块的方式处理数据,块I/O的效率比流IO高很多
-
BIO是阻塞的,NIO则是非阻塞的
-
BlO基于字节流和字符流进行操作,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道的事件(比如:连接请求,数据到达等),因此使用单个线程就可以监听多个客户端通道
-
NIO可以先将数据写入到缓冲区,然后再有缓冲区写入通道,因此可以做到同步非阻塞。
BIO则是面向的流,读写数据都是单向的。因此是同步阻塞。
NIO的三大核心部分
| 核心 | 对应类 | 应用 | 作用 |
|---|---|---|---|
| 缓冲区 | Buffer | 文件IO/网络IO | 存储数据 |
| 管道 | Channel | 文件IO/网络IO | 运输数据 |
| 选择器 | Selector | 网络IO | 控制 |
工作原理
Buffer(缓冲区)
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。相比较直接对数组的操作,Buffer APl更加容易操作和管理。
Channel(通道)
Java NIO的通道类似流,但又有些不同:既可以从通道中读取数据,又可以写数据到通道。但流的(input或output)读写通常是单向的。通道可以非阻塞读取和写入通道,通道可以支持读取或写入缓冲区,也支持异步地读写。
Selector(选择器)
Selector是一个ava NIO组件,可以能够检查一个或多个NIO通道,并确定哪些通道已经准备好进行读取或写入。这样,一个单独的线程可以管理多个channel,从而管理多个网络连接,提高效率
-
- 每个channel都会对应一个 Buffer - 一个线程对应Selector ,一个Selector对应多个channel(连接)程序 - 切换到哪个channel是由事件决定的 - Selector 会根据不同的事件,在各个通道上切换 - Buffer 就是一个内存块,底层是一个数组 - 数据的读取写入是通过 Buffer完成的,BlO中要么是输入流,或者是输出流,不能双向,但是NIO的Buffer是可以读也可以写。 - Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。通道表示打开到lO设备(例如:文件、套接字)的连接。若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。简而言之,Channel负责传输,Buffer负责存取数据
Buffer
-
Buffer有七种类型,Buffer是一个内存块,在NIO中所有的数据都是用Buffer处理,既可以读也可以写
-
Buffer 类他们都采用相似的方法进行管理数据,只是各自 管理的数据类型不同而已。都是通过如下方法获取一个 Buffer 对象:
static XxxBuffer allocate(int capacity) : 创建一个容量为capacity 的 XxxBuffer 对象
缓冲区的基本属性 Buffer 中的重要概念:
**容量 (capacity) :**作为一个内存块,Buffer具有一定的固定大小, 也称为"容量",缓冲区容量不能为负,并且创建后不能更改。
**限制 (limit):**表示缓冲区中可以操作数据的大小 (limit 后数据不能进行读写)。缓冲区的限制不能 为负,并且不能大于其容量。 写入模式,限制等于 buffer的容量。读取模式下,limit等于写入的数据量。
**位置 (position):**下一个要读取或写入的数据的索引。 缓冲区的位置不能为 负,并且不能大于其限制
**标记 (mark)与重置 (reset):**标记是一个索引, 通过 Buffer 中的 mark() 方法 指定 Buffer 中一个 特定的 position,之后可以通过调用 reset() 方法恢 复到这 个 position.
标记、位置、限制、容量遵守以下不变式: 0 <= mark <= position <= limit <= capacity
-
Buffer的常见方法
- **Buffer clear() :**清空缓冲区并返回对缓冲区的引用
- **Buffer flip() :**为 将缓冲区的界限设置为当前位置, 并将当前位置重置为 0
- **int capacity() :**返回 Buffer 的 capacity 大小
- boolean hasRemaining(): 判断缓冲区中是否还有元素
- **int limit() :**返回 Buffer 的界限(limit) 的位置
- Buffer limit(int n) 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象
- Buffer mark(): 对缓冲区设置标记
- **int position() :**返回缓冲区的当前位置 position
- **Buffer position(int n) :**将设置缓冲区的当前位置为 n, 并返回修改后的 Buffer 对象
- **int remaining() :**返回 position 和 limit 之间的元素个数
- **Buffer reset() :**将位置 position 转到以前设置的mark 所在的位置
- **Buffer rewind() :**将位置设为为 0, 取消设置的 mark
- **get() :**读取单个字节
- **get(byte[] dst):**批量读取多个字节到 dst 中
- **get(int index):**读取指定索引位置的字节(不会移动 position)放到入数据到Buffer中
- **put(byte b):**将给定单个字节写入缓冲区的当前位置
- **put(byte[] src):**将 src 中的字节写入缓冲区的当前位置
- **put(int index, byte b):**将指定字节写入缓冲区的索引 位置(不会移动 position)
-
使用Buffer读取数据的步骤
- 写入数据到Buffer
- 调用flip()方法,转换为读取模式
- 从Buffer中读取数据
- 调用buffer.clear()方法或者buffer.compact()方 法清除缓冲区
Channel
-
FileChannel,读写文件中的数据。 SocketChannel,通过TCP读写网络中的数据。 ServerSockectChannel,监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。 DatagramChannel,通过UDP读写网络中的数据。
-
Channel本身并不负责存储数据,只负责运输数据,必须配合buffer一起使用
-
获取通道的一种方式是对支持通道的对象调用getChannel() 方法。支持通道的类如下
- FileInputStream
- FileOutputStream
- RandomAccessFile
- DatagramSocket
- Socket
- ServerSocket
获取通道的其他方式是使用 Files 类的静态方法 newByteChannel() 获取字节通道。或者通过通道的静态方法 open() 打开并返回指定通道
-
FileChannel常用方法
- **int read(ByteBuffer dst) :**从Channel 到 中读取数据到 ByteBuffer
- long read(ByteBuffer[] dsts) : 将Channel中的数据“分散”到 ByteBuffer[]
- **int write(ByteBuffer src) :**将 ByteBuffer中的数据写入到 Channel
- **long write(ByteBuffer[] srcs) :**将 ByteBuffer[] 到 中的数据“聚集”到 Channel
- long position() :返回此通道的文件位置
- **FileChannel position(long p) :**设置此通道的文件位置
- long size() :返回此通道的文件的当前大小
- **FileChannel truncate(long s) :**将此通道的文件截取为给定大小
- **void force(boolean metaData) :**强制将所有对此通道的文件更新写入到存储设备中
Selector
-
Selector`翻译成选择器,有些人也会翻译成多路复用器,实际上指的是同一样东西。
只有网络IO才会使用选择器,文件IO是不需要使用的。
选择器可以说是NIO的核心组件,它可以监听通道的状态,来实现异步非阻塞的IO。换句话说,也就是事件驱动。以此实现单线程管理多个Channel的目的。
-
核心API
- Selector.open():打开一个选择器
- select(): 选择一组键,其相应的通道已为 I/O 操作准备就绪。
- selectedKeys():返回此选择器的已选择键集
管道间的数据传输
transferTo():把源通道的数据传输到目的通道中。
transferFrom():把来自源通道的数据传输到目的通道。
// 例子
// 把源通道的数据传输到目的通道中。
inputStreamChannel.transferTo(0, byteBuffer.limit(), outputStreamChannel);
// 把来自源通道的数据传输到目的通道。
outputStreamChannel.transferFrom(inputStreamChannel,0,byteBuffer.limit());
分散读取和聚合写入
可以通过一个缓冲区数组读取管道数据,这就叫分散读取
也可以将一个缓冲区数组的数据写入管道,这就叫聚合写入
public static void main(String[] args) throws Exception {
//获取文件输入流
File file = new File("1.txt");
FileInputStream inputStream = new FileInputStream(file);
//从文件输入流获取通道
FileChannel inputStreamChannel = inputStream.getChannel();
//获取文件输出流
FileOutputStream outputStream = new FileOutputStream(new File("2.txt"));
//从文件输出流获取通道
FileChannel outputStreamChannel = outputStream.getChannel();
//创建三个缓冲区,分别都是5
ByteBuffer byteBuffer1 = ByteBuffer.allocate(5);
ByteBuffer byteBuffer2 = ByteBuffer.allocate(5);
ByteBuffer byteBuffer3 = ByteBuffer.allocate(5);
//创建一个缓冲区数组
ByteBuffer[] buffers = new ByteBuffer[]{byteBuffer1, byteBuffer2, byteBuffer3};
//循环写入到buffers缓冲区数组中,分散读取
long read;
long sumLength = 0;
while ((read = inputStreamChannel.read(buffers)) != -1) {
sumLength += read;
Arrays.stream(buffers)
.map(buffer -> "posstion=" + buffer.position() + ",limit=" + buffer.limit())
.forEach(System.out::println);
//切换模式
Arrays.stream(buffers).forEach(Buffer::flip);
//聚合写入到文件输出通道
outputStreamChannel.write(buffers);
//清空缓冲区
Arrays.stream(buffers).forEach(Buffer::clear);
}
System.out.println("总长度:" + sumLength);
//关闭通道
outputStream.close();
inputStream.close();
outputStreamChannel.close();
inputStreamChannel.close();
}
- 使用场景就是可以使用一个缓冲区数组,自动地根据需要去分配缓冲区的大小。可以减少内存消耗。
直接与非直接缓冲区
-
ByteBuffer有两种:HeapByteBuffer与DirectByteBuffer
-
其实根据类名就可以看出,
HeapByteBuffer所创建的字节缓冲区就是在JVM堆中的,即JVM内部所维护的字节数组。而DirectByteBuffer是直接操作操作系统本地代码创建的内存缓冲数组。DirectByteBuffer的使用场景:- java程序与本地磁盘、socket传输数据
- 大文件对象,可以使用。不会受到堆内存大小的限制。
- 不需要频繁创建,生命周期较长的情况,能重复使用的情况。
HeapByteBuffer的使用场景:除了以上的场景外,其他情况还是建议使用
HeapByteBuffer,没有达到一定的量级,实际上使用DirectByteBuffer是体现不出优势的。 -
// 非直接缓冲区的创建方式 static ByteBuffer allocate(int capacity) // 直接缓冲区的创建方式: static ByteBuffer allocateDirect(int capacity)
从示意图中我们可以发现,最大的不同在于直接缓冲区不需要再把文件内容copy到物理内存中。这就大大地提高了性能。其实在介绍Buffer时,我们就有接触到这个概念。直接缓冲区是堆外内存,在本地文件IO效率会更高一点。
网络IO
其实NIO的主要用途是网络IO,在NIO之前java要使用网络编程就只有用Socket。而Socket是阻塞的,显然对于高并发的场景是不适用的。所以NIO的出现就是解决了这个痛点。
主要思想是把Channel通道注册到Selector中,通过Selector去监听Channel中的事件状态,这样就不需要阻塞等待客户端的连接,从主动等待客户端的连接,变成了通过事件驱动。没有监听的事件,服务器可以做自己的事情。
-
示例
-
服务端
public class NIOServer { public static void main(String[] args) throws Exception { //打开一个ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666); //绑定地址 serverSocketChannel.bind(address); //设置为非阻塞 serverSocketChannel.configureBlocking(false); //打开一个选择器 Selector selector = Selector.open(); //serverSocketChannel注册到选择器中,监听连接事件 serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); //循环等待客户端的连接 while (true) { //等待3秒,(返回0相当于没有事件)如果没有事件,则跳过 if (selector.select(3000) == 0) { System.out.println("服务器等待3秒,没有连接"); continue; } //如果有事件selector.select(3000)>0的情况,获取事件 Set<SelectionKey> selectionKeys = selector.selectedKeys(); //获取迭代器遍历 Iterator<SelectionKey> it = selectionKeys.iterator(); while (it.hasNext()) { //获取到事件 SelectionKey selectionKey = it.next(); //判断如果是连接事件 if (selectionKey.isAcceptable()) { //服务器与客户端建立连接,获取socketChannel SocketChannel socketChannel = serverSocketChannel.accept(); //设置成非阻塞 socketChannel.configureBlocking(false); //把socketChannel注册到selector中,监听读事件,并绑定一个缓冲区 socketChannel.register(selector, SelectionKey.OP_READ, ByteBuffer.allocate(1024)); } //如果是读事件 if (selectionKey.isReadable()) { //获取通道 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); //获取关联的ByteBuffer ByteBuffer buffer = (ByteBuffer) selectionKey.attachment(); //打印从客户端获取到的数据 socketChannel.read(buffer); System.out.println("from 客户端:" + new String(buffer.array())); } //从事件集合中删除已处理的事件,防止重复处理 it.remove(); } } } } -
客户端
public class NIOClient { public static void main(String[] args) throws Exception { SocketChannel socketChannel = SocketChannel.open(); InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666); socketChannel.configureBlocking(false); //连接服务器 boolean connect = socketChannel.connect(address); //判断是否连接成功 if(!connect){ //等待连接的过程中 while (!socketChannel.finishConnect()){ System.out.println("连接服务器需要时间,期间可以做其他事情..."); } } String msg = "hello java技术爱好者!"; ByteBuffer byteBuffer = ByteBuffer.wrap(msg.getBytes()); //把byteBuffer数据写入到通道中 socketChannel.write(byteBuffer); //让程序卡在这个位置,不关闭连接 System.in.read(); } }
-
-
SelectionKey
- 在
SelectionKey类中有四个常量表示四种事件,来看源码:
public abstract class SelectionKey { //读事件 public static final int OP_READ = 1 << 0; //2^0=1 //写事件 public static final int OP_WRITE = 1 << 2; // 2^2=4 //连接操作,Client端支持的一种操作 public static final int OP_CONNECT = 1 << 3; // 2^3=8 //连接可接受操作,仅ServerSocketChannel支持 public static final int OP_ACCEPT = 1 << 4; // 2^4=16 }附加的对象(可选),把通道注册到选择器中时可以附加一个对象。
public final SelectionKey register(Selector sel, int ops, Object att)从
selectionKey中获取附件对象可以使用attachment()方法public final Object attachment() { return attachment; } - 在