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

posted @ 2012-11-25 22:40  寂静沙滩  阅读(1429)  评论(0编辑  收藏  举报