概述
NIO的关键组成包括三个部分:channel(通道)、buffer(缓存区)和selector(选择区)。传统IO是基于字节流和字符流的,而NIO则是基于通道和缓存区的,数据可以从通道读取到缓存区,也可以从缓存区写入到通道。
NIO和传统IO区别
channel和传统IO的InputStream、OutputStream比较相似,但是传统的IO是单向的,只能写或者只能读。而NIO的channel是双向的。
传统IO是面向字节流和字符流的,每次只能读取一个或者多个字节,需要记录读取了多少个字节数,而NIO是面向缓存区的。
传统IO没法动态移动已经读取的数据的位置,如果想从数据的某个特定位置获取数据,需要将这些数据先缓存起来,在指定位置读取;而NIO是可以动态指定读取数据的位置,灵活性高。
传统IO是阻塞式的,比如socket的accept(),当没有连接时线程会一直阻塞;而NIO是非阻塞式的,当通道没有就绪时,线程可以去做其它事情不用一直阻塞等待。
channel
NIO中的channel是双向的,这是和传统的IO中的FileInputStream、FileOutputStream的最本质的区别
channel最常用的有以下四种
- FileChannel (文件IO)
- DatagramChannel (UDP)
- SocketChannel (TCP服务器)
- ServerSocketChannel (TCP客户端)
buffer
buffer是NIO中的关键,常用的buffer有以下几种
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- LongBuffer
- IntBuffer
- ShortBuffer
- MappedByteBuffer
具体用法后面会讲到
selector
selector采用的是使用单线程去访问多个channel渠道,对于渠道数量比较多,但是访问流量少的场景selector最实用。
selector会轮询检查各个channel是否已经准备就绪,前提是每个channel要先register到这个selector中,然后调用selector的select的方法,这个时候单线程会轮询访问各个channel,如果channel没有准备好就返回,如果有准备就绪的channel,则线程就会立刻去执行。
在讲解案例之前,首先还是来了解一下buffer的一些属性和内部原理,这样有助于我们讲解和理解案例代码
Java NIO中的Buffer用于和NIO通道进行交互。如你所知,数据是从通道读入缓冲区,从缓冲区写入到通道中的。
缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。
Buffer的用法
buffer的用法主要有四种
写入数据
调用flip()方法
读取数据
调用clear()或者compact()清空数据
首先channel向buffer中写入数据,当数据写完后读取数据时,首先调用flip方法切换模式,将buffer从写模式切换到读模式(具体切换细节后面会讲到),然后开始去读数据。
当数据读取结束后需要清空buffer,而清空buffer有两种方式,一种是clear它会将buffer中的数据全部抹去(实际上数据是存在的,只是记录数据的位置清零,后面会详细说明);而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,首先要了解它的这三个属性
capacity: buffer的大小
position
limit
position和limit的含义取决于当前是写入还是读取,而capacity的含义是固定的,就是buffer的大小。
写入数据
当写入数据的时候,position的位置是0,而limit的位置和capacity的位置一样,指向当前buffer的最大位置 每次写入一个数据,position的位置就会+1
读取数据
当调用slip方法后由写模式转换为读模式,这个时候position会指向位置0,即第一个可以读取数据的位置;而limit则指向刚才position的位置,也就是接下来可以写入数据的位置,而capacity仍然指向的是buffer的最大位置。 也就是说position和limit直接的区域就是可以读取的数据区域,也是有数据的区域。
其它方法
mark()和reset()
mark()用于记录当前的position的一个特定位置,后面可以通过reset()方法恢复到这个特定位置
rewind()
rewind() 用于将position的位置重置为0,而limit的位置不变,这样做的目的可以让我们重新读取buffer中的数据。
equals()和compareTo()
可以使用equals()和compareTo()方法比较两个Buffer。 * equals() 当满足下列条件时,表示两个Buffer相等: 有相同的类型(byte、char、int等)。 Buffer中剩余的byte、char等类型的个数相等。 Buffer中所有剩余的byte、char等类型都相同。 如你所见,equals只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素。 * compareTo()方法 compareTo()方法比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer: 第一个不相等的元素小于另一个Buffer中对应的元素 。 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。
clear()与compact()
一旦读完Buffer中的数据,需要让Buffer准备好再次被写入。可以通过clear()或compact()方法来完成。 如果调用的是clear()方法,position将被设回0,limit被设置成 capacity的值。换句话说,Buffer 被清空了。Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据。 如果Buffer中有一些未读的数据,调用clear()方法,数据将“被遗忘”,意味着不再有任何标记会告诉你哪些数据被读过,哪些还没有。 如果Buffer中仍有未读的数据,且后续还需要这些数据,但是此时想要先先写些数据,那么使用compact()方法。 compact()方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。现在Buffer准备好写数据了,但是不会覆盖未读的数据。