nio原理与实例

 

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 scSet = selector.selectedKeys(); Iterator iter = scSet.iterator(); while (iter.hasNext()) { SelectionKey key = (SelectionKey) iter.next(); iter.remove(); dispatch(key); } } } catch (IOException e) { e.printStackTrace(); } } } public void dispatch(SelectionKey key) { // 有客户端连接请求时 //if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) { if (key.isAcceptable()) { try { System.out.println("Key is acceptable"); ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel socket = (SocketChannel) ssc.accept(); socket.configureBlocking(false); socket.register(selector, SelectionKey.OP_READ); } catch (IOException e) { e.printStackTrace(); } } // 从客户端读取数据 //else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { else if (key.isReadable()) { System.out.println("the key is readable"); // new Thread(new ReadeHandler(key)).start(); try { SocketChannel socket = (SocketChannel) key.channel(); ByteBuffer buffer = ByteBuffer.allocate(1024); int bytesRead = socket.read(buffer); if (bytesRead > 0) { buffer.flip(); // 将字节转化为为UTF-16的字符串 String receivedString = Charset.forName("UTF-16").newDecoder().decode(buffer).toString(); // 控制台打印出来 System.out.println("接收到来自" + socket.socket() .getRemoteSocketAddress() + "的信息:" + receivedString); // 准备发送的文本 String sendString = "你好,客户端. @" + new Date().toString() + ",已经收到你的信息:" + receivedString; buffer = ByteBuffer.wrap(sendString .getBytes("UTF-16")); socket.write(buffer); // 设置为下一次读取或是写入做准备 key.interestOps(SelectionKey.OP_READ); } } 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(); } } } // 客户端可写时 else if (key.isWritable()) { System.out.println("tHe key is writable"); //new Thread(new WriteHandler(key)).start(); //do something } } public static void main(String[] args) throws IOException { Server server = new Server(9911); server.start(); } }  

这是一个守候在端口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空闲时,即为可写;有数据来时,可读。空闲状态下,所有的通道都是可写的,如果你给每个通道注册了写事件,那么非常容易造成死循环。所以一般不使用写通知。

 

你可能感兴趣的:(Java)