Java NIO非堵塞应用通常适用用在I/O读写等方面,我们知道,系统运行的性能瓶颈通常在I/O读写,包括对端口和文件的操作上,过去,在打开一个I/O通道后,read()将一直等待在端口一边读取字节内容,如果没有内容进来,read()也是傻傻的等,这会影响我们程序继续做其他事情,那么改进做法就是开设线程,让线程去等待,但是这样做也是相当耗费资源的。
Java NIO非堵塞技术实际是采取Reactor模式,或者说是Observer模式为我们监察I/O端口,如果有内容进来,会自动通知我们,这样,我们就不必开启多个线程死等,从外界看,实现了流畅的I/O读写,不堵塞了。
Java NIO出现不只是一个技术性能的提高,你会发现网络上到处在介绍它,因为它具有里程碑意义,从JDK1.4开始,Java开始提高性能相关的功能,从而使得Java在底层或者并行分布式计算等操作上已经可以和C或Perl等语言并驾齐驱。
如果你至今还是在怀疑Java的性能,说明你的思想和观念已经完全落伍了,Java一两年就应该用新的名词来定义。从JDK1.5开始又要提供关于线程、并发等新性能的支持,Java应用在游戏等适时领域方面的机会已经成熟,Java在稳定自己中间件地位后,开始蚕食传统C的领域。
本文主要简单介绍NIO的基本原理,在下一篇文章中,将结合Reactor模式和著名线程大师Doug Lea的一篇文章深入讨论。
NIO主要原理和适用。
NIO 有一个主要的类Selector,这个类似一个观察者,只要我们把需要探知的socketchannel告诉Selector,我们接着做别的事情,当有事件发生时,他会通知我们,传回一组SelectionKey,我们读取这些Key,就会获得我们刚刚注册过的socketchannel,然后,我们从这个Channel中读取数据,放心,包准能够读到,接着我们可以处理这些数据。
Selector内部原理实际是在做一个对所注册的channel的轮询访问,不断的轮询(目前就这一个算法),一旦轮询到一个channel有所注册的事情发生,比如数据来了,他就会站起来报告,交出一把钥匙,让我们通过这把钥匙来读取这个channel的内容。
import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; import java.util.Date; import java.util.Iterator; import java.util.Set; public class Server{ private final Selector selector; private final ServerSocketChannel serverSocketChannel; public Server(int port) throws IOException{ // 创建选择器 selector = Selector.open(); // 打开监听信道 serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress adress = new InetSocketAddress(InetAddress.getLocalHost(),port); //与本地端口绑定 serverSocketChannel.socket().bind(adress); // 设置为非阻塞模式 serverSocketChannel.configureBlocking(false); // 注册选择器.并在注册过程中指出该信道可以进行Accept操作 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); } public void start() { System.out.println("the server is started......"); while (true) { try { int nKeys = selector.select(); if (nKeys > 0){ // selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey Set
这是一个守候在端口9011的noblock server例子,如果我们编制一个客户端程序,就可以对它进行互动操作,或者使用telnet 主机名 9911 可以链接上。
import java.io.IOException; import java.net.InetAddress; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.SelectionKey; import java.nio.channels.Selector; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; public class Client { // 信道选择器 private Selector selector; // 与服务器通信的信道 SocketChannel socketChannel; public Client(int port)throws IOException{ selector = Selector.open(); socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(),port)); socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); // 启动读取线程 new Thread(new ClientReadThread()).start(); } //发送字符串到服务器 public void sendMsg(String message) throws IOException{ ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes("UTF-16")); socketChannel.write(writeBuffer); } public static void main(String[] args) throws IOException{ Client client = new Client(9911); client.sendMsg("你好!Nio!"); } class ClientReadThread implements Runnable{ public void run() { try { while (selector.select() > 0) { // 遍历每个有可用IO操作Channel对应的SelectionKey for (SelectionKey sk : selector.selectedKeys()) { // 如果该SelectionKey对应的Channel中有可读的数据 if (sk.isReadable()) { // 使用NIO读取Channel中的数据 SocketChannel sc = (SocketChannel) sk.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); sc.read(buffer); buffer.flip(); // 将字节转化为为UTF-16的字符串 String receivedString = Charset.forName("UTF-16").newDecoder().decode(buffer).toString(); // 控制台打印出来 System.out.println("接收到来自服务器" + sc.socket().getRemoteSocketAddress() + "的信息:" + receivedString); // 为下一次读取作准备 sk.interestOps(SelectionKey.OP_READ); } // 删除正在处理的SelectionKey selector.selectedKeys().remove(sk); } } } catch (IOException ex) { ex.printStackTrace(); } } } }
注意的是:
在客户端主动关闭连接之后,按理说服务端在调用Selector的select方法时候应该是阻塞的,但是我的测试代码中却仍然能够返回,而且返回的SelectionKey的isReadable方法返回的仍然是key,导致死循环。
原因是 当客户端主动切断连接时,FD_READ仍然起作用,也就是说,状态仍然是有东西可读,不过读出来的字节是0,所以需要判断客户端是否已经断开:
链接断开后,虽然该channel的ready operation是OP_READ,但是此时channel.read(buffer)返回-1,此时可以增加一个判断
if (socketChannel.read(buffer) != -1) { buffer.flip(); System.out.println(Charset.defaultCharset().decode(buffer)); } else { System.out.println("client socket is closed"); socketChannel.close(); }
但是这种方法在我的测试代码中没有起作用,最终的解决方法是使用异常捕捉:
catch (IOException e) { //客户端断开连接,所以从Selector中取消注册 key.cancel(); if(key.channel() != null) try { key.channel().close(); System.out.println("the client socket is closed!"); } catch (IOException e1) { e1.printStackTrace(); } }
另外,对于写通知,socket空闲时,即为可写;有数据来时,可读。空闲状态下,所有的通道都是可写的,如果你给每个通道注册了写事件,那么非常容易造成死循环。所以一般不使用写通知。