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的内容。
1 import java.io.IOException; 2 import java.net.InetAddress; 3 import java.net.InetSocketAddress; 4 import java.nio.ByteBuffer; 5 import java.nio.channels.SelectionKey; 6 import java.nio.channels.Selector; 7 import java.nio.channels.ServerSocketChannel; 8 import java.nio.channels.SocketChannel; 9 import java.nio.charset.Charset; 10 import java.util.Date; 11 import java.util.Iterator; 12 import java.util.Set; 13 public class Server{ 14 private final Selector selector; 15 private final ServerSocketChannel serverSocketChannel; 16 17 public Server(int port) throws IOException{ 18 // 创建选择器 19 selector = Selector.open(); 20 // 打开监听信道 21 serverSocketChannel = ServerSocketChannel.open(); 22 InetSocketAddress adress = new InetSocketAddress(InetAddress.getLocalHost(),port); 23 //与本地端口绑定 24 serverSocketChannel.socket().bind(adress); 25 // 设置为非阻塞模式 26 serverSocketChannel.configureBlocking(false); 27 // 注册选择器.并在注册过程中指出该信道可以进行Accept操作 28 serverSocketChannel.register(selector,SelectionKey.OP_ACCEPT); 29 } 30 public void start() { 31 System.out.println("the server is started......"); 32 while (true) { 33 try { 34 int nKeys = selector.select(); 35 if (nKeys > 0){ 36 // selectedKeys()中包含了每个准备好某一I/O操作的信道的SelectionKey 37 Set<SelectionKey> scSet = selector.selectedKeys(); 38 Iterator<SelectionKey> iter = scSet.iterator(); 39 while (iter.hasNext()) { 40 SelectionKey key = (SelectionKey) iter.next(); 41 iter.remove(); 42 dispatch(key); 43 } 44 } 45 } catch (IOException e) { 46 e.printStackTrace(); 47 } 48 } 49 } 50 51 public void dispatch(SelectionKey key) { 52 // 有客户端连接请求时 53 //if ((key.readyOps() & SelectionKey.OP_ACCEPT) == SelectionKey.OP_ACCEPT) { 54 if (key.isAcceptable()) { 55 try { 56 System.out.println("Key is acceptable"); 57 ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); 58 SocketChannel socket = (SocketChannel) ssc.accept(); 59 socket.configureBlocking(false); 60 socket.register(selector, SelectionKey.OP_READ); 61 } catch (IOException e) { 62 e.printStackTrace(); 63 } 64 } 65 // 从客户端读取数据 66 //else if ((key.readyOps() & SelectionKey.OP_READ) == SelectionKey.OP_READ) { 67 else if (key.isReadable()) { 68 System.out.println("the key is readable"); 69 // new Thread(new ReadeHandler(key)).start(); 70 try { 71 SocketChannel socket = (SocketChannel) key.channel(); 72 ByteBuffer buffer = ByteBuffer.allocate(1024); 73 int bytesRead = socket.read(buffer); 74 if (bytesRead > 0) { 75 buffer.flip(); 76 // 将字节转化为为UTF-16的字符串 77 String receivedString = Charset.forName("UTF-16").newDecoder().decode(buffer).toString(); 78 // 控制台打印出来 79 System.out.println("接收到来自" 80 + socket.socket() 81 .getRemoteSocketAddress() 82 + "的信息:" + receivedString); 83 // 准备发送的文本 84 String sendString = "你好,客户端. @" 85 + new Date().toString() + ",已经收到你的信息:" 86 + receivedString; 87 buffer = ByteBuffer.wrap(sendString 88 .getBytes("UTF-16")); 89 socket.write(buffer); 90 // 设置为下一次读取或是写入做准备 91 key.interestOps(SelectionKey.OP_READ); 92 } 93 } catch (IOException e) { 94 //客户端断开连接,所以从Selector中取消注册 95 key.cancel(); 96 if(key.channel() != null) 97 try { 98 key.channel().close(); 99 System.out.println("the client socket is closed!"); 100 } catch (IOException e1) { 101 e1.printStackTrace(); 102 } 103 } 104 } 105 // 客户端可写时 106 else if (key.isWritable()) { 107 System.out.println("tHe key is writable"); 108 //new Thread(new WriteHandler(key)).start(); 109 //do something 110 } 111 } 112 113 public static void main(String[] args) throws IOException { 114 Server server = new Server(9911); 115 server.start(); 116 } 117 }
这是一个守候在端口9011的noblock server例子,如果我们编制一个客户端程序,就可以对它进行互动操作,或者使用telnet 主机名 9911 可以链接上。
1 import java.io.IOException; 2 import java.net.InetAddress; 3 import java.net.InetSocketAddress; 4 import java.nio.ByteBuffer; 5 import java.nio.channels.SelectionKey; 6 import java.nio.channels.Selector; 7 import java.nio.channels.SocketChannel; 8 import java.nio.charset.Charset; 9 public class Client { 10 // 信道选择器 11 private Selector selector; 12 // 与服务器通信的信道 13 SocketChannel socketChannel; 14 15 public Client(int port)throws IOException{ 16 selector = Selector.open(); 17 socketChannel = SocketChannel.open(new InetSocketAddress(InetAddress.getLocalHost(),port)); 18 socketChannel.configureBlocking(false); 19 socketChannel.register(selector, SelectionKey.OP_READ); 20 // 启动读取线程 21 new Thread(new ClientReadThread()).start(); 22 } 23 //发送字符串到服务器 24 public void sendMsg(String message) throws IOException{ 25 ByteBuffer writeBuffer = ByteBuffer.wrap(message.getBytes("UTF-16")); 26 socketChannel.write(writeBuffer); 27 } 28 29 public static void main(String[] args) throws IOException{ 30 Client client = new Client(9911); 31 client.sendMsg("你好!Nio!"); 32 } 33 34 class ClientReadThread implements Runnable{ 35 public void run() { 36 try { 37 while (selector.select() > 0) { 38 // 遍历每个有可用IO操作Channel对应的SelectionKey 39 for (SelectionKey sk : selector.selectedKeys()) { 40 // 如果该SelectionKey对应的Channel中有可读的数据 41 if (sk.isReadable()) { 42 // 使用NIO读取Channel中的数据 43 SocketChannel sc = (SocketChannel) sk.channel(); 44 ByteBuffer buffer = ByteBuffer.allocate(1024); 45 sc.read(buffer); 46 buffer.flip(); 47 // 将字节转化为为UTF-16的字符串 48 String receivedString = Charset.forName("UTF-16").newDecoder().decode(buffer).toString(); 49 // 控制台打印出来 50 System.out.println("接收到来自服务器" + sc.socket().getRemoteSocketAddress() + "的信息:" + receivedString); 51 // 为下一次读取作准备 52 sk.interestOps(SelectionKey.OP_READ); 53 } 54 // 删除正在处理的SelectionKey 55 selector.selectedKeys().remove(sk); 56 } 57 } 58 } catch (IOException ex) { 59 ex.printStackTrace(); 60 } 61 } 62 } 63 }
注意的是:
在客户端主动关闭连接之后,按理说服务端在调用Selector的select方法时候应该是阻塞的,但是我的测试代码中却仍然能够返回,而且返回的SelectionKey的isReadable方法返回的仍然是key,导致死循环。
原因是 当客户端主动切断连接时,FD_READ仍然起作用,也就是说,状态仍然是有东西可读,不过读出来的字节是0,所以需要判断客户端是否已经断开:
链接断开后,虽然该channel的ready operation是OP_READ,但是此时channel.read(buffer)返回-1,此时可以增加一个判断
1 if (socketChannel.read(buffer) != -1) { 2 buffer.flip(); 3 System.out.println(Charset.defaultCharset().decode(buffer)); 4 } else { 5 System.out.println("client socket is closed"); 6 socketChannel.close(); 7 }
但是这种方法在我的测试代码中没有起作用,最终的解决方法是使用异常捕捉:
1 catch (IOException e) { 2 //客户端断开连接,所以从Selector中取消注册 3 key.cancel(); 4 if(key.channel() != null) 5 try { 6 key.channel().close(); 7 System.out.println("the client socket is closed!"); 8 } catch (IOException e1) { 9 e1.printStackTrace(); 10 } 11 }
另外,对于写通知,socket空闲时,即为可写;有数据来时,可读。空闲状态下,所有的通道都是可写的,如果你给每个通道注册了写事件,那么非常容易造成死循环。所以一般不使用写通知。