nio

nio学习笔记

Posted by alonealice on 2018-02-01

在学习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
2
RandomAccessFile aFile = new RandomAccessFile(filePath, "rw");
FileChannel inChannel = aFile.getChannel();

缓存区

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
2
3
4
buf.flip();  //将buffer切换到读模式
while(buf.hasRemaining()){
System.out.print((char) buf.get()); //逐个读取数据
}

选择器

Selector允许单线程处理多个 通道。当单线程中需要处理多个连接时,且连接数量较多时,适合这种情况。当用单个线程来处理多个通道时,只需要更少的线程来处理,甚至可以只用一个线程来处理所有的通道。对于操作系统来说,线程之间上下文切换的开销很大,而且每个线程都要占用系统的一些资源。因此,使用的线程越少越好。

注册通道和使用

通道和选择器配合使用,必须要将通道注册到选择器上。

1
2
3
4
Selector selector = Selector.open();//开启选择器
channel.configureBlocking(false);
SelectionKey key = channel.register(selector,
Selectionkey.OP_READ);

这里有几个点需要注意一下,首先是与选择器一起使用的通道必须是非阻塞模式的,因此FileChannel不能与selector一起使用,因为FileChannel不能切换到非阻塞模式;第二是在注册时需要传入注册监听的事件的类型,可以监听一下事件:connectAcceptReadwrite,使用时既可以使用其中的一种,也可以多种。如:

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
Selector selector = Selector.open();
channel.configureBlocking(false);
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
while(true) {
int readyChannels = selector.select();
if(readyChannels == 0) continue;
Set selectedKeys = selector.selectedKeys();
Iterator keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
}