(NIO常在网络通信中被使用,故笔者在这里以java网络通信为例讲解NIO)
说到NIO,这几个重要的角色就必须要提及
1.Selector(选择器)
2.Channel(管道)
3.Buffer(缓冲区)
NIO常常被称作非阻塞IO,这缘于其在java网络通信中的非阻塞特性被普遍使用。
Buffer缓冲区的构造如图所示:
(将Buffer理解为一个数组,以向缓冲区中写入为例:)
其中,Position表示允许开始写入的数组索引,limit表示允许写入的最大索引限制,Capacity为Buffer缓冲区的容量。
假设现在Buffer中有5个数据,如图:
调用Buffer对象的flip()方法,可以转换Buffer的读写属性(即,原本向Buffer中写入,现在从Buffer中读取,调用flip()方法后,Buffer如下图所示:)
我们可以看到,Buffer中的数据并没有发生变化,只是Posotion和limit指向的位置发生了变化,position回到Buffer头部,limit移动到原来Posotion所在的位置上,我们现在从Buffer中读取时
依旧是从Position所在的位置开始读取
Selector与Channel相结合,以多路复用的方式大大提高了java网络通信的IO效率。
Selector作为Channel的管理器,允许多条通道同时连接到Selector管理器(专业术语称注册到选择器)
其中,每个Channel都可以设置自己关心的事件(比如,可读、可写),在指定的事件被触发后,Selector管理器将会将IO资源向指定Channel分配,在Channel完成这一件IO事件之后,Selector会重新收回IO资源,等待下一次触发指定Channel兴趣事件的情况。示例代码如下:
//建立一个Selector管理器
Selector selector = Selector.open();
ServerSocketChannel ssChannel = ServerSocketChannel.open();
//设置ssChannel为非阻塞,使得之后该ServerSocketChannel上的SocketChannel的accept方法为非阻塞
ssChannel.configureBlocking(false);
//将ssChannel注册到selector管理器上,并且设置感兴趣事件为客户端的连接
SelectionKey ssKey=ssChannel.register(selector, SelectionKey.OP_ACCEPT);
InetSocketAddress address = new InetSocketAddress("127.0.0.1", 6666);
ssChannel.bind(address);
在将管道注册到管理器上并设置了兴趣事件之后就要开始等待IO事件,操实现思路为在一个while循环中,使用Selector的select方法等待兴趣事件的发生,有了兴趣事件之后,将这一次接受到的所有事件放入一个迭代器中,依次执行迭代器中的每一个元素(迭代器中每一个元素都是SelectionKey类型的对象,通过这个对象可以对注册到Selector上的Channel进行操作),对元素执行后需要将其从迭代器中移除。示例代码如下:
while (true) {
selector.select();//阻塞等待兴趣事件的发生
Set<SelectionKey> keys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = keys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
keyIterator.remove();//将处理过的IO任务从迭代器中移除
try{
if (key.isAcceptable()) {//有客户端请求连接时触发
ServerSocketChannel ssChannel1 = (ServerSocketChannel) key.channel();
SocketChannel sChannel = ssChannel1.accept();
//将sChannel设置为非阻塞,使得该管道中的读写操作为非阻塞
sChannel.configureBlocking(false);
//开辟一个大小为10个字节的缓冲区
ByteBuffer buffer =ByteBuffer.allocate(10);
//将管道注册到selector管理器上,并且设置感兴趣事件为读事件,并且将之前创建的buffer缓冲区作为一个附件和该sChannel绑定到一起
sChannel.register(selector, SelectionKey.OP_READ,buffer);
} else if (key.isReadable()) {//服务器管道读端收到来自客户端发送的数据
//通过key获取指定Channel管道
SocketChannel sChannel = (SocketChannel) key.channel();
int read=0;
ByteBuffer buffer=(ByteBuffer) key.attachment();
if(sChannel.isOpen()){
//将数据读取到sChannel的附件buffer上
read=sChannel.read(buffer);
}
if(read==-1){//为了防止客户端关闭后程序陷入死循环,在检测到指定管道的对端关闭后,将该管道的注册事件取消,即,从Selector管理器上移除该管道
key.cancel();
}else{
buffer.flip();
sChannel.write(buffer);
buffer.clear();
}
}
}catch(IOException ex){//当该元素对应的管道上出现IO异常,将该管道的注册事件取消
ex.printStackTrace();
key.cancel();
key.channel().close();
}
}
}
在示例程序中,需要重点考虑客户端关闭时的情况:
当客户端异常关闭时,服务端会引发一个IO异常,我们如果没有进行处理,服务器会因为这个异常而瘫痪,无法在接受其他客户端的连接,很显然,这种情况是我们不愿意看到的,所以我们需要将客户端关闭时引发的IO异常捕获,这样服务器就不会因此瘫痪。捕捉异常之后,我们必须在捕获到异常之后将当前SelectionKey元素从兴趣列表中取消,并且将对应管道关闭。
当客户端关闭时,客户端会给服务器发送一个可读取的信号,服务端会进行读取,但是客户端已经关闭,服务端进行读取时会失败,read()方法将返回-1,表示管道对端关闭。但是如果没有读到内容,该管道的读取事件会使得select()操作一直被触发,这就会引发服务端陷入死循环。因此,我们必须对Channel对象的read()方法返回值进行判断,当其为-1时,我们需要将当前SelectionKey元素从兴趣列表中取消,只有当其不为-1时,我们才可以认为Channel读取成功,并且对buffer中的内容进行操作。
(经笔者测试,在archlinux下运行,无论客户端正常关闭还是异常关闭Channel的read()都会方法都会返回-1,而且客户端在被异常关闭时不会引发异常)
写在最后:由于笔者目前NIO了解程度较浅,这篇文章若有不足之处,欢迎读者进行斧正。