在学习nio之前,首先要弄清楚什么是nio,以及它与平时使用的io的区别。
NIO(Non-blocking I/O,在Java领域,也称为New I/O),是一种同步非阻塞的I/O模型,也是I/O多路复用的基础。它已经被越来越多地应用到了大型应用服务器中,是一种解决高并发与大量连接、I/O处理问题的有效方式。
我们都知道,IO是面向流的, 这面向流意味着每次可以从流中读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。此外,它不能前后移动流中的数据。如果需要前后移动从流中读取的数据,需要先将它缓存到一个缓冲区。 同时IO也是阻塞的。当一个线程调用read() 或 write()时,该线程是会被阻塞的,直到有一些数据被读取,或数据完全写入。该线程在此期间不能再干任何事情了,但是cpu却是空闲的。
而NIO是面向缓冲区的,读取数据读时会将数据放到一个缓冲区,需要时可在缓冲区中前后移动。同时,它还会检查是否该缓冲区中包含所有您需要处理的数据。当读入更多的数据到缓冲区时,要保证不会覆盖缓冲区里尚未处理的数据。
在nio中有几个重要的概念,通道,缓冲区和选择器。
通道
java中的通道类似流,我们既可以冲通道中读取数据,又可以将数据写到通道,但是流的读取写入都是单向的。同时,通道可以异步读写,读写数据时总是先读到一个缓冲区,或者将数据写入一个缓冲区。
nio中重要的通道有:FileChannel
从文件中读写数据、DatagramChannel
能通过UDP读写网络中的数据、SocketChannel
能通过TCP读写网络中的数据、ServerSocketChannel
可以监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel。
1 | RandomAccessFile aFile = new RandomAccessFile(filePath, "rw"); |
缓存区
Buffer用于和NIO通道进行交互,本质上是一块可以写入数据和读取数据的内存。这块内存被包装成NIO Buffer对象后,提供了一组方法,用来方便的访问该块内存。
它有3个重要的属性:
capacity:Buffer有一个固定的大小值capacity。当往里面存储数据时满时,需要通过读取和清除等方式将数据清除才能往里面继续存储数据。
position:position表示当前的位置。初始的position值为0.当写入一个数据时, position会向前移动到下一个可插入数据的Buffer单元。position最大可为capacity – 1。当读取数据时,也是从某个特定位置读。当将Buffer从写模式切换到读模式,position会被重置为0. 当从Buffer的position处读取数据时,position向前移动到下一个可读的位置。
limit:在写模式下,Buffer的limit表示你最多能往Buffer里写多少数据。 写模式下,limit等于Buffer的capacity。当切换Buffer到读模式时, limit表示你最多能读到多少数据。因此,当切换Buffer到读模式时,limit会被设置成写模式下的position值。换句话说,你能读到之前写入的所有数据。
Buffer的类型
Java NIO 有以下Buffer类型:ByteBuffer、MappedByteBuffer、CharBuffer、DoubleBuffer、FloatBuffer、IntBuffer、LongBuffer、ShortBuffer。
Buffer的使用
buffer读写数据时首先需要分配一块内存大小, 每一个Buffer类都有一个allocate方法,大小值就是capacity值。
1 | ByteBuffer buf = ByteBuffer.allocate(48); |
之后需要将数据写入到buffer中:
1 | int bytesRead = inChannel.read(buf); |
写入之后需要将buffer的模式有写换到读,然后读取数据:
1 | buf.flip(); //将buffer切换到读模式 |
选择器
Selector允许单线程处理多个 通道。当单线程中需要处理多个连接时,且连接数量较多时,适合这种情况。当用单个线程来处理多个通道时,只需要更少的线程来处理,甚至可以只用一个线程来处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源。因此,使用的线程越少越好。
注册通道和使用
通道和选择器配合使用,必须要将通道注册到选择器上。
1 | Selector selector = Selector.open();//开启选择器 |
这里有几个点需要注意一下,首先是与选择器一起使用的通道必须是非阻塞模式的,因此FileChannel不能与selector一起使用,因为FileChannel不能切换到非阻塞模式;第二是在注册时需要传入注册监听的事件的类型,可以监听一下事件:connect
、Accept
、Read
和write
,使用时既可以使用其中的一种,也可以多种。如:
1 | SelectionKey.OP_READ | SelectionKey.OP_WRITE |
注册完通道后,就可以调用elect()方法,返回已经准备好的通道。
select()
阻塞到至少有一个通道在你注册的事件上就绪了。
select(long timeout)
和select()一样,除了最长会阻塞timeout毫秒(参数)。
selectNow()
不阻塞,只要有通道准备好就立刻返回,如果没有就返回0。
select()方法返回准备好的新通道数量,就是说如果是返回上次调用select方法之后的准备好的通道。调用了select()方法之后,如果有一个或更多个通道就绪了,然后可以通过调用selector的selectedKeys()方法,获取已经准备好的通道集合。
1 | Set selectedKeys = selector.selectedKeys(); |
然后可以遍历通道集合,根据通道不同的状态做相应的处理。在处理完之后,需要调用iterator.remove(),因为selector不会自己从已选择键集中移除SelectionKey实例。必须在处理完通道时自己移除。下次该通道变成就绪时,Selector会再次将其放入已选择键集中。
1 | Selector selector = Selector.open(); |