java之NIO
1 什么是NIO
Java NIO (New IO,Non-Blocking IO)是从Java 1.4版本开始引入的一套新的IO API。NIO支持面向缓冲区的、基于通道的IO操作。NIO的三大核心部分:通道(Channel),缓冲区(Buffer), 选择器(Selector),数据总是从通道读取到缓冲区,或者从缓冲区写入到通道中,选择器用于监听多个通道事件,如连接打开,数据到达等。Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer),Channel负责传输,Buffer负责存储数据。
BIO与NIO的理解:传统IO即BIO在进行数据传输时必须要建立一个连接才能进行数据的写入和读取。可以吧数据理解为水流,需要有管道,可以认为应用程序和文件之间的连接就是一个管道用来运输水流。输入流和输出流是不同的管道,他们是单向的。NIO在进程应用程序和文件之间数据传输时他们的连接不能理解为管道,他有个概念为“通道”,可以理解为铁轨,还有“缓冲区”可以理解为火车。起到运输作用,但是本事不能进行运输数据,数据的运输需要借助于火车。当我们要读取磁盘文件的时候数据会先加载到缓冲区,然后传输到应用程序。
2 BIO与NIO的区别
(1)BIO是面向流,流是单向的。每次从流中读取一个或者多个字节,直到读取完所有字节,没有被缓存起来,不能前后移动流中的数据,如果想要能前后移动的话需要将他缓存到另外一个缓冲区;NIO是面向缓冲区的,通道可以将数据读取到缓存区实现双向传输。NIO是将数据读取到一个稍后处理的缓冲区,并且在需要的时候可以前后移动。
(2)BIO是阻塞式,一个线程调用read()或者write()的时候这个线程被阻塞,直到数据被读取或者完全写入,不能再干其他事情;NIO是非阻塞式,一个线程从一个通道发送请求读取数据,只能获取到目前可用的,没数据可用就什么都不会获取,不保持阻塞,直到数据变得可以读取之前,这个线程可以做其他事,写也是这样。非阻塞IO的线程在空闲时间作用在其他通道上执行IO操作,那么一个线程就可以管理多个输入输出通道。
(3)BIO传输的是字节流或字符流,NIO是通过块传输。
面向文件IO的区别:BIO是面向流的,NIO是面向缓冲区的。面向网络IO的区别:BIO是阻塞的,NIO是非阻塞的,并且NIO有选择器
3 缓冲区(Buffer)
3.1 缓冲区相关概念
通道表示IO源到 IO 设备(例如:文件、套接字)的连接。若需要使用 NIO 系统,需要获取用于连接 IO 设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。
缓冲区(Buffer):Buffer主要用于与Channel交互,数据从Channel写入Buffer,然后再从Buffer写出到Channel。是一个用于特定基本数据类型(除boolean型外)的容器,底层使用数组存储,可以保存多个相同类型的数据。所有缓冲区都是java.nio.buffer的子类,常见的子类有ByteBuffer,CharBuffer,IntBuffer,DoubleBuffer,ShortBuffer,LongBuffer,FloatBuffer等,他们管理数据的方法都相似,管理的类型不同而已。
Buffer的实现类都是通过allocate(int,capacity)创建一个容量为capacity的对象。Buffer有以下基本属性:
容量(capacity) | 标识Buffer存储的最大数据容量,声明后不能更改,不能为负,通过capacity()获取 |
限制(limit) | 第一个不应该读取或写入的数据的索引,也就是limit后的数据不可以读写,不能为负,不能大于capacity,通过limit()获取 |
位置(position) | 当前要读取或者写入数据的索引,不能为负,不能大于limit,通过position()获取 |
标记(mark) | 标记是一个索引,通过mark()标记后,可以通过调用reset()将position恢复到标记的mark处 |
上述属性的范围大小为: 0 <= mark <= position <= limit <= capacity
3.2 缓冲区的基本操作
缓冲区为所有的子类提供了两个用于数据操作的方法put和get方法,如ByteBuffer的这两个方法如下
方法 | 说明 |
put(byte b) | 将指定的单个字节写入缓冲区的当前位置 |
put(byte[] buf) | 将buf中的字节写入缓冲区的当前位置 |
put(int index,byte b) | 将指定字节写入缓冲区的索引位置,不移动position |
get() | 读取单个字节 |
get(byte[] buf) | 批量读取多个字节到buf中 |
get(int index) | 读取指定索引位置的字节,不移动position |
Buffer其他常用方法
方法 | 说明 |
Buffer flip() | 将limit设置为当前position,position设置为0,mark设置为-1 |
Buffer rewind() | 将position设置为0,mark设置为-1,可以重复读 |
Buffer clear() | 将limit设置为capacity,position设置为0,mark设置为-1,数据没有清空 |
Buffer mark() | 设置缓冲区的mark |
Buffer reset() | 将当前位置的position转到之前设置的mark的位置 |
Buffer hasRemaining() | 判断缓冲区中是否还有元素 |
int remaining | 返回position和limit之间元素的个数 |
Xxx[] array() | 返回XxxBuffer底层的Xxx数组 |
int capacity() | 返回Buffer的capacity大小 |
int limit() | 返回Buffer的limit位置 |
Buffer limit(int n) | 将设置缓冲区界限为 n, 并返回一个具有新 limit 的缓冲区对象 |
int position() | 返回Buffer的position位置 |
Buffer position(int n) | 将设置缓冲区的当前位置为 n , 并返回修改后的 Buffer 对象 |
说明:①当我们调用ByteBuffer.allocate(10)方法创建了一个10个byte的数组的缓冲区,position的位置为0,capacity和limit默认都是数组长度。②当通过put方法写入5个字节到缓冲区时,position更新为5。③需要将缓冲区中的5个字节数据写入Channel的通信信道,调用ByteBuffer.flip()方法,变化为position设回0,并将limit设成之前的position的值④这时底层操作系统就可以从缓冲区中正确读取这个5个字节数据并发送出去了。在下一次写数据之前我们再调用clear()方法,缓冲区的索引位置又回到了初始位置。
注意:clear()是把position设回0,limit设置成capacity,换句话说,其实Buffer中的数据没有清空,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。如果Buffer中有一些未读的数据,调用clear()方法,数据将丢弃,那就没有标记说明哪些数据读过,哪些还没有。如果还需要Buffer中未读的数据,但是还想要先写些数据,那么使用compact()方法。compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。
public static void main(String[] args) {/*** 通过allocate()获取缓冲区,缓冲区主要有2个核心方法:put()将输入存入缓冲区,get()获取缓冲区数据*/// 获取缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 将数据存入缓冲区String str = "hello";buffer.put(str.getBytes());System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 获取缓冲区数据,要获取缓存区的数据需要flip()切换缓冲区的模式buffer.flip();System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// 创建字节数据接收数据byte[] b = new byte[buffer.limit()];buffer.get(b);System.out.println(new String(b,0,buffer.limit()));System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());// rewind()可重复读buffer.rewind();System.out.println("capacity:"+ buffer.capacity()+"\t position:"+buffer.position()+"\t limit:"+buffer.limit());}
-----------------------------------
输出结果:
capacity:1024 position:0 limit:1024
capacity:1024 position:5 limit:1024
capacity:1024 position:0 limit:5
hello
capacity:1024 position:5 limit:5
capacity:1024 position:0 limit:5
3.3 直接缓冲区和非直接缓冲区
缓冲区分为直接缓冲区和非直接缓存区:①非直接缓冲区:硬盘-->系统的缓冲区-->copy-->JVM缓冲区-->程序②直接缓冲区:需要copy,JVM和缓冲区实现映射。
直接字节缓冲区, Java 虚拟机会尽最大努力直接在此缓冲区上执行本机 I/O 操作。也就是说在每次调用基础操作系统的一个本机 I/O 操作之前(或之后),虚拟机都会尽量避免将缓冲区的内容复制到中间缓冲区中(或从中间缓冲区中复制内容)。
直接字节缓冲区可以通过调用ByteBuffer的 allocateDirect() 工厂方法来创建。此方法返回的缓冲区进行分配和取消分配所需成本通常高于非直接缓冲区。直接缓冲区的内容可以驻留在常规的垃圾回收堆之外,因此,它们对应用程序的内存需求量造成的影响可能并不明显。所以,建议将直接缓冲区主要分配给那些易受基础系统的本机 I/O 操作影响的大型、持久的缓冲区。一般情况下,最好仅在直接缓冲区能在程序性能方面带来明显好处时分配它们。
直接字节缓冲区还可以通过 FileChannel 的 map() 方法 将文件区域直接映射到内存中来创建。该方法返回ByteBuffer的子类:MappedByteBuffer 。Java 平台的实现有助于通过 JNI 从本机代码创建直接字节缓冲区。如果以上这些缓冲区中的某个缓冲区实例指的是不可访问的内存区域,则试图访问该区域不会更改该缓冲区的内容,并且将会在访问期间或稍后的某个时间导致抛出不确定的异常。
非直接缓冲区如上,假设应用程序想要在磁盘中读取一些数据的话。应用程序首先发起一个请求,要去读取当前物理磁盘里面的数据,这个时候需要将物理磁盘的数据首先读到内核空间中,然后拷贝一份到用户空间,然后才能通过read的方法将数据读到应用程序中。同样的应用程序中有数据的话需要先写入到用户地址空间,然后复制到内核地址空间,再由内核空间写入到物理磁盘。在这个过程中,这两个复制的操作比较多余,所以他的效率比较低一些,也就是将我们的缓冲区建立在jvm的内存中相对效率更低。
直接字节缓冲区如上图,直接缓冲区不需要拷贝,是将我们的数据直接在物理内存中建立一个映射文件,将数据写到这个文件里面去,这个时候我们应用程序要写一些数据的话直接写到这个映射文件。操作系统就会将这个写到物理磁盘中。读磁盘数据同理。这个过程就没有中间的copy就会比较高。
字节缓冲区是直接缓冲区还是非直接缓冲区可通过调用其 isDirect() 方法来确定。非直接缓冲区:通过allocate方法分区缓存区,将缓存区建立在JVM的内存中
直接缓存区:通过allocateDircet方法分区缓冲区,将缓冲区建立在物理内存中,效率更高。
到allocateDirect和allocate创建的源码中,发现allocate创建的是一个HeapByteBuffer,Heap堆其实就是表示用户空间,在jvm内存中创建的一个缓冲区。
public static ByteBuffer allocate(int capacity) {if (capacity < 0)throw new IllegalArgumentException();return new HeapByteBuffer(capacity, capacity);}public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity);}
3.4 缓冲区使用
Buffer使用一般遵循以下几个原则:
①分配空间,如ByteBuffer buffer = ByteBuffer.allocate(1024);或者使用allocateDirector
②将数据写入到Buffer中 int readBuffer = inChannel.read(buffer);
③调用flip()方法,将limit设置为当前position,position设置为0,mark设置为-1
④从Buffer中读取数据 readBuffer = inChannel.read(buffer);
⑤调用clear()(将limit设置为capacity,position设置为0,mark设置为-1,数据没有清空)方法或者compact()方法
4 通道(Channel)
Channel表示IO源于目标节点打开的连接,类似于传统的流,但是Channel不直接存储数据,Channel只能与Buffer进行交互。
以非直接缓冲区为例,应用程序获取数据需要经过用户空间,然后内核空间,再读取数据,所有的读取操作在NIO是直接由CPU负责的。这个流程会存在一些问题,当我们有大量的文件读取操作的时候cpu他的利用就很低,因为IO操作直接抢占CPU的资源,就不能够去做其他的事情,所以他的效率就会变低。
计算机CPU和内存的交互是最频繁的,内存是我们的高速缓存区,CPU运转速度越来越快,磁盘远远跟不上CPU的读写速度,才设计了内存。这里把CPU的连接干掉了,变成了DMA(Direct Memory Access,直接内存存取器),就是直接内存存储。如果要读取数据,所以的操作是直接在当前DMA这里直接完成,不再有CPU去进行负责。但是得到DMA还是需要由当前的CPU进行相关的调度。在这里交给了DMA之后,CPU就能做其他的事,但是如果依然有大量的IO操作的时候又会造成DMA总线的拥堵,因为最终没有直接和CPU撇开关系。导致在大量的文件读取请求的时候依然利用率比较低,这个时候就出现了新的数据读写流程,这个时候就出现了channel通道。
把DMA换成了通道channel。通道channel可以认为他是一个完全独立的处理器,他就是用来专门负责文件的IO操作的,也就是说以后所有的数据直接交给channel去进行负责读取。这个时候CPU才算是真正的解放了。
java为Channel接口提供的最主要的实现类如下:①FileChannel:用于读取,写入、映射和操作文件的通道②SocketChannel:通过TCP读取网络中的数据③ServerSocketChannel:可以监听新进来的TCP连接,对每个新进来的连接都会创建一个SocketChannel④DatagramChannel:通过UDP读写网络中的数据通道
获取通道的三种方式:①对支持通道的对象调用getChannel(),支持通道的类有:FileInputStream,FileOutputStream,RandomAccessFile,Socket,ServerSocket,DatagramSocket②通过XxxChannel的静态方法open()打开并返回指定的XxxChannel③使用Files工具类的静态方法newByteChannel()获取字节通道。
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 |
MappedByteBuffer map(MapMode mode,long position,long size) | 将Channel对应的部分数据或者全部数据映射到ByteBuffer |
long position() | 返回次通道的文件位置 |
FileChannel position(long p) | 设置此通道的文件位置 |
long size() | 返回此通道的文件大小 |
FileChannel truncate(long s) | 将此通道的文件截取为给定大小 |
void force(boolean metadata) | 强制将所有对此通道的文件更新写入到存储设备中 |
分散(Scatter)读取和聚集(Gather)写入:①分散读取(Scattering Reads是指从Channel中读取的数据“分散”到多个Buffer中,注意:按照缓冲区的顺序,从 Channel 中读取的数据依次将Buffer填满。②聚集写入(Gathering Writes)是指将多个Buffer中的数据“聚集”到Channel,注意:按照缓冲区的顺序,写入position和limit之间的数据到Channel。
NIO的强大功能部分来自于Channel的非阻塞特性,套接字的某些操作可能会无限期地阻塞。如对accept()方法的调用可能会因为等待一个客户端连接而阻塞;对read()方法的调用可能会因为没有数据可读而阻塞,直到连接的另一端传来新的数据。总的来说,创建/接收连接或读写数据等I/O调用,都可能无限期地阻塞等待,直到底层的网络实现发生了什么。慢速的,有损耗的网络,或仅仅是简单的网络故障都可能导致任意时间的延迟。然而不幸的是,在调用一个方法之前无法知道其是否阻塞。NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现非阻塞式的信道。 channel.configureBlocking(false)
在非阻塞式信道上调用一个方法总是会立即返回。这种调用的返回值指示了所请求的操作完成的程度。例如,在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连接请求来了,则返回客户端SocketChannel,否则返回null。
对比传统IO和NIO的代码
/*** 传统IO*/public static void IO_FileInputStream(){BufferedInputStream bis = null;BufferedOutputStream bos = null;try {bis = new BufferedInputStream(new FileInputStream(new File("a.txt")));bos = new BufferedOutputStream(new FileOutputStream(new File("b.txt")));byte[] buffer = new byte[1024];int len;while ((len=bis.read(buffer))!=-1){bos.write(buffer,0,len);bos.flush();}} catch (IOException e) {e.printStackTrace();}finally {try {if(bis != null){bis.close();}if(bos != null){bos.close();}} catch (IOException e) {e.printStackTrace();}}}/*** NIO*/public static void NIO_FileInputStream(){FileInputStream fis = null;FileOutputStream fos = null;try {fis = new FileInputStream(new File("a.txt"));fos = new FileOutputStream(new File("b.txt"));FileChannel inChannel = fis.getChannel();FileChannel outChannel = fos.getChannel();ByteBuffer buffer = ByteBuffer.allocate(1024);int readBuffer = inChannel.read(buffer);while (readBuffer!=-1){buffer.flip();while(buffer.hasRemaining()){outChannel.write(buffer);}buffer.compact();readBuffer = inChannel.read(buffer);}} catch (IOException e) {e.printStackTrace();}finally{try{if(fis != null){fis.close();}if(fos != null){fos.close();}}catch (IOException e){e.printStackTrace();}}}
对比直接缓冲区与内存映射文件操作
public class NioTest {public static void main(String[] args) {nioBuffer();nioDirectBuffer();}private static void nioBuffer() {long start = System.currentTimeMillis();FileChannel inChannel = null;FileChannel outChannel = null;try {// 获取通道inChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz"), StandardOpenOption.READ);outChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz.bak"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);// 创建缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);// 将输入通道的数据写入缓冲区while (inChannel.read(buffer)!=-1){buffer.flip();// 将缓冲区数据写入输出通道outChannel.write(buffer);// 清空缓冲区buffer.clear();}} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();}finally {if (outChannel!=null){try {outChannel.close();} catch (IOException e) {e.printStackTrace();}}if (inChannel!=null){try {inChannel.close();} catch (IOException e) {e.printStackTrace();}}}long end = System.currentTimeMillis();System.out.println("nioBuffer:"+(end-start));}private static void nioDirectBuffer() {long start = System.currentTimeMillis();FileChannel inChannel = null;FileChannel outChannel = null;try {// 获取通道inChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz"), StandardOpenOption.READ);outChannel = FileChannel.open(Paths.get("D:\\test\\doneFile0comlog_20201117_01.log.gz.bak"),StandardOpenOption.WRITE,StandardOpenOption.READ,StandardOpenOption.CREATE);// 进行内存映射文件MappedByteBuffer inMapBuffer = inChannel.map(FileChannel.MapMode.READ_ONLY, 0, inChannel.size());MappedByteBuffer outMapBuffer = outChannel.map(FileChannel.MapMode.READ_WRITE, 0, outChannel.size());// 对缓冲区进行读写操作byte[] b = new byte[inMapBuffer.limit()];inMapBuffer.get(b);outMapBuffer.put(b);} catch (FileNotFoundException e) {e.printStackTrace();} catch (IOException e) {e.printStackTrace();} finally {if (outChannel != null) {try {outChannel.close();} catch (IOException e) {e.printStackTrace();}}if (inChannel != null) {try {inChannel.close();} catch (IOException e) {e.printStackTrace();}}}long end = System.currentTimeMillis();System.out.println("nioDirectBuffer:" + (end - start));}}----------------------------------
结果为:
nioBuffer:94
nioDirectBuffer:7
5 选择器(Selector )
5.1 相关概念
NIO和BIO有一个非常大的区别是BIO是阻塞的,NIO是非阻塞的。阻塞与非阻塞是相对于网络通信而言的。网络通信就会有客户端的概念。客户端要向服务端发送数据的话必须建立连接,在这个过程中会做一些相关的事情,如accpet等待连接,然后客户端write数据,服务端read数据。这些操作在传统的套接字socket里面都是阻塞式的。服务端一次只能接待一个客户端,不能一下多个的客户端。也就是客户端请求服务器做些事情的时候,这个客户端没有处理完,其他客户端的请求是进不来的。这种就是阻塞式的,所以服务端如果是这种模型的话,他的效率是非常低的。
要解决这种阻塞就要通过多线程的方式解决,但是线程资源是有限的,那就极大的限制了服务端他的处理效率。这就是经典的C10K问题,假如有C10K,就需要创建1W个进程。在NIO中非阻塞的网络通信模型Selector就能解决这个问题。
系统线程的切换是消耗系统资源的,如果我们每一个连接都用一个线程来管理,资源的开销会非常大,这个时候就可以用Selector。通过Selector可以实现一个线程管理多个Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用Selector就会很方便。例如在一个聊天服务器中。要使用Selector, 得向Selector注册Channel,然后调用它的select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件(如新的连接进来、数据接收等)。Selector 的意义在于只通过一个线程就可以管理成千上万个 I/O 请求, 相比使用多个线程,避免了线程上下文切换带来的开销。
Selector是怎么工作的呢?有了Selector之后,Selector会把每一个客户端和服务端传输数据的通道都到Selector上去注册一下。也就是以后你想向服务端发送数据,通道先到Selector选择器上注册一下,那么Selector就会监控当前channel的IO状况(读,写,连接,接受处理等情况)只有当某个channel上的数据完全准备就绪,Selector才会把这样一个channel里面的任务分配到服务端来进行运行。当我们客户端要给服务端发送数据的时候,channel需要在Selector上进行注册,当channel的数据完全准备就绪的时候Selector才会将任务分配给服务端的一个线程进行处理。这种非阻塞式的相较于阻塞式的就能非常好的利用cpu的资源,提高cpu的工作效率。
一个Selector实例可以同时检查一组信道的I/O状态。用专业术语来说,选择器就是一个多路开关选择器,因为一个选择器能够管理多个信道上的I/O操作。然而如果用传统的方式来处理这么多客户端,使用的方法是循环地一个一个地去检查所有的客户端是否有I/O操作,如果当前客户端有I/O操作,则可能把当前客户端扔给一个线程池去处理,如果没有I/O操作则进行下一个轮询,当所有的客户端都轮询过了又接着从头开始轮询;这种方法是非常笨而且也非常浪费资源,因为大部分客户端是没有I/O操作,我们也要去检查;而Selector就不一样了,它在内部可以同时管理多个I/O,当一个信道有I/O操作的时候,他会通知Selector,Selector就是记住这个信道有I/O操作,并且知道是何种I/O操作,是读呢?是写呢?还是接受新的连接;所以如果使用Selector,它返回的结果只有两种结果,一种是0,即在你调用的时刻没有任何客户端需要I/O操作,另一种结果是一组需要I/O操作的客户端,这时你就根本不需要再检查了,因为它返回给你的肯定是你想要的。这样一种通知的方式比那种主动轮询的方式要高效得多。
使用选择器,首先创建一个Selector实例(使用静态工厂方法open())并将其注册(register)到想要监控的信道上(通过channel的方法实现,而不是使用selector的方法)。最后,调用选择器的select()方法。该方法会阻塞等待,直到有一个或更多的信道准备好了I/O操作或等待超时。select()方法将返回可进行I/O操作的信道数量。现在,在一个单独的线程中,通过调用select()方法就能检查多个信道是否准备好进行I/O操作。如果经过一段时间后仍然没有信道准备好,select()方法就会返回0,并允许程序继续执行其他任务。
Selector 只能与非阻塞模式下的通道一起使用(即需要实现 SelectableChannel 接口),否则会抛出 IllegalBlockingModeException 异常
5.2 Selector使用
(1)使用步骤
①创建Selector
②向Selector注册通道,一个Selector可以注册多个通道
③通过Selector选择就绪的通道
// 通过open()方法创建 SelectorSelector selector = Selector.open();// 创建一个通道,以ServerSockeetChannel为例,并且将通道设置为非阻塞模式ServerSocketChannel channel = ServerSocketChannel.open();channel.configureBlocking(false);// 通过register()方法注册通道channel.register(selector, SelectionKey.OP_ACCEPT);// 通过select()方法从多个通道中以轮询的方式选择已经准备就绪的通道。根据之前register()方法中设置的兴趣,将可以进行对应操作的通道选择出来selector.select();// 通过Selector的selectedKeys()方法获得已选择键集(selected-key set)Set key = selector.selectedKeys();//通过Iterator迭代器依次获取key中的SelectionKey对象,并通过SelectionKey中的判断方法执行对应的操作Iterator<SelectionKey> iterator = key.iterator();while (iterator.hasNext()) {SelectionKey selectionKey = iterator.next();if (selectionKey.isAcceptable()) {//TODO}if (selectionKey.isReadable()){//TODO}if(selectionKey.isWritable()&&key.isValid()){//TODO}if (selectionKey.isConnectable()){//TODO}iterator.remove();}
(2)register()方法
public abstract SelectionKey register(Selector sel, int ops, Object att)throws ClosedChannelException;
register() 方法返回SelectionKey对象,在SelectableChannel抽象类中定义如上。参数说明如下
Selector sel | 通道注册的选择器 |
int ops | interest集合,表示通过Selector监听Channel时对什么事件感兴趣 |
Object att | 这是一个可选参数,在注册通道时可以附加一个对象,用于之后便于识别某个通道 |
interest集合有下面4种操作
操作类型 | 值 | 描述 |
SelectionKey.OP_ACCEPT | 1<<4 | 接收Socket操作 |
SelectionKey.OP_READ | 1<<0 | 读操作 |
SelectionKey.OP_WRITE | 1<<2 | 写操作 |
SelectionKey.OP_CONNECT | 1<<3 | 接收Socket操作 |
注意:通道一般并不会同时支持这四种操作类型,我们可以通过 validOps() 方法获取通道支持的类型。
(3)select()方法
select有2个重载方法:
①int select():选择已准备就绪的通道,返回值表示自上一次选择后有多少新增通道准备就绪;当没有通道准备就绪时,会一直阻塞下去,直到至少一个通道被选择、该选择器的 wakeup() 方法被调用或当前线程被中断时。select() 方法实际上调用了 select(0L) 方法返回
②int select(long timeout):选择已准备就绪的通道;当没有通道准备就绪时,会一直阻塞下去,直到至少一个通道被选择、该选择器的 wakeup() 方法被调用、当前线程被中断或给定时间到期时返回。
除此紫外还可以选择 selectNow() 方法,该方法为非阻塞方法,无论有无通道就绪都会立即返回。如果自前一次 select 操作后没有新的通道准备就绪,则会立即返回 0。
(4)SelectionKey
SelectionKey中有下面几种判断方法,与操作类型相对应:
boolean isReadable() | 是否可读,是返回 true |
boolean isWritable() | 是否可写,是返回 true |
boolean isConnectable() | 是否可连接,是返回 true |
boolean isAcceptable() | 是否可接收,是返回 true |
selectedKeys() 获得的是已就绪的通道对应的 SelectionKey。如果想获得该选择器上所有通道对应的 SelectionKey,可以通过 keys() 方法获取。
(5)使用例子
public class NIOServer {public static void main(String[] args) throws IOException {// 获取通道,并设置为非阻塞ServerSocketChannel ssChannel = ServerSocketChannel.open();ssChannel.configureBlocking(false);// 绑定端口号ssChannel.bind(new InetSocketAddress(9999));// 创建选择器对象Selector selector = Selector.open();// 将通道注册到选择器上,那么选择器就会监听通道的接收时间,如果有接收,并且接收准备就绪才开始进行下一步操作ssChannel.register(selector, SelectionKey.OP_ACCEPT);// 通过轮训的方式获取选择器上准备就绪的事件// selector.select()>0表示至少有个selectionKey准备就绪while (selector.select()>0){// 获取当前选择器中所有注册的选择键Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();// 迭代获取已经准备好的选择键while (iterator.hasNext()){// 获取已经准备就是的事件SelectionKey sk = iterator.next();if(sk.isAcceptable()){// 调用accpetSocketChannel sChannel = ssChannel.accept();// 将sChannel设置为非阻塞的sChannel.configureBlocking(false);// 将该通道注册到选择器上sChannel.register(selector,SelectionKey.OP_READ);}else if(sk.isReadable()){// 如果读状态已经准备就是,那么开始读取数据// 获取当前选择器上读状态准备就绪的通道SocketChannel sChannel = (SocketChannel)sk.channel();// 创建缓冲区接收客户端发送过来的数据ByteBuffer buffer = ByteBuffer.allocate(1024);// 读取缓冲区的数据int len =0;while ((len=sChannel.read(buffer))>0){buffer.flip();System.out.println(new String(buffer.array(),0,len));buffer.clear();}}// 当selectKey使用完之后要溢出,否则会一直优先iterator.remove();}}}
}public class NIOClient {public static void main(String[] args) throws IOException {// 获取通道,默认是阻塞的SocketChannel sChannel = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));// 设置通道为非阻塞的sChannel.configureBlocking(false);// 创建缓冲区ByteBuffer buffer = ByteBuffer.allocate(1024);buffer.put("hello".getBytes());// 将缓冲区数据写入到sChannel中buffer.flip();sChannel.write(buffer);buffer.clear();sChannel.close();}
}