导航

Java NIO

Posted on 2013-10-23 11:40  酷鱼影子  阅读(175)  评论(0编辑  收藏  举报


NIO是New IO的缩写,顾名思义,是用于输入输出的新的API,那么,这个NIO相较于旧的IO有什么差别呢?

   1、“阻塞”的通信机制

    在原有的IO下,我们要与A进行通信时,会怎么做呢?先创建一个线程,然后建立连接,然后不断轮询等待接收消息。当需要与另一个B进行通信时,仍然先创建一个线程,然后建立连接(accept),不断轮询等待接收消息(read)… 在这种情况下,若通信的对象变多时,需要的线程就相应增长,并且每个线程都需要不断轮询等待,若没有消息接受时,则阻塞线程。由于消息传送的频率往往不高,所以,线程大部分时间都处于阻塞状态。而阻塞态的线程仍占用着系统资源,多个线程的管理及线程状态的切换(阻塞-就绪-运行)是比较耗时的。

    由于这种方式在一个通信任务没有完成之前,是无法返回的,所以以上的这种方式称作“阻塞”的通信机制。

   2、观察者模式

    “阻塞”的通信机制,从本质上来看,就是每个通信的线程都需要询问“是否有我的数据?”,如果没有则将该线程阻塞,当有数据到来时,再唤醒线程。每个线程都在观察某个事物,等待自己的事件发生。这个描述,与观察者模式类似。我们可以使用观察者模式来进一步优化“阻塞”的通信机制。我们需要监听多个端口时,传统方式是建立多个线程进行监听。而现在,我们将每个端口封装为一个通道,所有传入该端口的数据都会出现在通道中,假设有3个需要监听的端口,则有3个通道,我们对这三个通道进行监听,使用Selector来注册我们对各个端口监听的行为。比如,对第一个端口(通道),需要监听他是否有可读数据,对于第二个端口(通道)需要监听他是否有accept请求… Selector记录了我们对各个通道的兴趣点,然后统一的对各个端口进行监听。当有相应的事件发生时,则调用相关的处理函数。这样,就完成了只用一个监听线程完成多个端口监听的任务。这种方式,就是“非阻塞”的通信机制。


   3、nio 的关键

    我们来想象一下读取文件的方式。首先,若直接从一个文件中读取数据,每次读取一个字符,若文件中有10个字符,那么就需要进行10次IO,而每一次IO都是很耗时的。为了减少IO的次数,我们使用了buffer,假定每次读取的数据都先存入buffer中,每次读取5个字符。那么,从buffer中间接读取字符,尽管同样是10次,但只需要2次的IO就够了,减少了IO次数,提高了效率。

    为了能够完成端到端的传输,需要有一定的媒介进行。就像计算机系统中,数据传输是通过总线一样,nio中,数据的传输是通过“通道”进行的。例如,需要将文件A的内容传到(写入)文件B,那么,通过buffer与通道,可以如下操作:



    在这里,通道作为中间的传输媒介,Buffer则附加在通道的两端,作为数据的“来源”与目标(真正的应该是File A与File B)。使用通道,则一方(File A)则只需要通过Buffer往通道里写,而另一方(File B)则只需要通过从通道里读即可,而无需关注二者的差异。

    在NIO中,通道的一端绑定了相应的目的或者源头,例如文件、socket等,而另一端则是让使用buffer来获取或者写入数据。如下图:


缓冲区的工作与通道紧密联系。通道是I/O 传输发生时通过的入口,而缓冲区是这些数据传输的来源或目标。

    NIO中,有以下几个关键类型

    1)缓存buffer

    NIO中,提供了各个基本类型的buffer,提供给我们以不同的方式进行读写。例如可以用ByteBuffer进行字节(8bit为单位)的数据读写,使用CharByte进行字符(16bit为单位)的数据读写。

    2)通道Channel

    NIO中有以下几种通道:SocketChannel、ServerSocketChannel、DiagramChannel和FileChannel。

    FileChannel只能通过FileOutputStream或者FileInputStream的getChannel调用得到,是单向的,即一端与相应的文件绑定,另一端进行write或者read。

    ServerSocketChannel用于服务器端的通道创建,绑定了一个端口号后,所有接受的数据都会在该通道中,配合后面的Selector,可以直接获取封装好(SelectionKey)的某个客户发送的数据。

    虽说通道既可读又可写,但实际上,一个绑定了某个源或者目的的通道,是只有读或写的功能的。

    3)选择器Selector

    他是使得多元I/O成为可能的关键,使用Selector来注册对通道的某些行为的关注,使之可以同时管理监控多个通道,当监控的时间发生时,唤醒并调用统一的事件处理函数进行相应。

    选择器Selector:选择器类管理着一个被注册的通道集合的信息和它们的就绪状态。通道是和选择器一起被注册的,并且使用选择器来更新通道的就绪状态。当这么做的时候,可以选择将被激发的线程挂起,直到有就绪的的通道。

    可选择通道SelectableChannel:这个抽象类提供了实现通道的可选择性所需要的公共方法。它是所有支持就绪检查的通道类的父类。

选择键SelectionKey:选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register()  返回并提供一个表示这种注册关系的标记。选择键包含了两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。

 

附上网上的一个NIO,Server和Client的代码:

 

  1. <span style="font-size:18px">package nio;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.InetSocketAddress;  
  5. import java.nio.ByteBuffer;  
  6. import java.nio.channels.SelectionKey;  
  7. import java.nio.channels.Selector;  
  8. import java.nio.channels.ServerSocketChannel;  
  9. import java.nio.channels.SocketChannel;  
  10. import java.util.Iterator;  
  11.   
  12. /** 
  13.  * NIO服务端 
  14.  * @author 小路 
  15.  */  
  16. public class NIOServer {  
  17.     //通道管理器  
  18.     private Selector selector;  
  19.   
  20.     /** 
  21.      * 获得一个ServerSocket通道,并对该通道做一些初始化的工作 
  22.      * @param port  绑定的端口号 
  23.      * @throws IOException 
  24.      */  
  25.     public void initServer(int port) throws IOException {  
  26.         // 获得一个ServerSocket通道  
  27.         ServerSocketChannel serverChannel = ServerSocketChannel.open();  
  28.         // 设置通道为非阻塞  
  29.         serverChannel.configureBlocking(false);  
  30.         // 将该通道对应的ServerSocket绑定到port端口  
  31.         serverChannel.socket().bind(new InetSocketAddress(port));  
  32.         // 获得一个通道管理器  
  33.         this.selector = Selector.open();  
  34.         //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,  
  35.         //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。  
  36.         serverChannel.register(selector, SelectionKey.OP_ACCEPT);  
  37.     }  
  38.   
  39.     /** 
  40.      * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 
  41.      * @throws IOException 
  42.      */  
  43.     @SuppressWarnings("unchecked")  
  44.     public void listen() throws IOException {  
  45.         System.out.println("服务端启动成功!");  
  46.         // 轮询访问selector  
  47.         while (true) {  
  48.             //当注册的事件到达时,方法返回;否则,该方法会一直阻塞  
  49.             System.out.println("开始阻塞");  
  50.             selector.select();  
  51.             System.out.println("解除阻塞");  
  52.             // 获得selector中选中的项的迭代器,选中的项为注册的事件  
  53.             Iterator ite = this.selector.selectedKeys().iterator();  
  54.             while (ite.hasNext()) {  
  55.                 SelectionKey key = (SelectionKey) ite.next();  
  56.                 // 删除已选的key,以防重复处理  
  57.                 ite.remove();  
  58.                 // 客户端请求连接事件  
  59.                 if (key.isAcceptable()) {  
  60.                     ServerSocketChannel server = (ServerSocketChannel) key  
  61.                             .channel();  
  62.                     // 获得和客户端连接的通道  
  63.                     SocketChannel channel = server.accept();  
  64.                     // 设置成非阻塞  
  65.                     channel.configureBlocking(false);  
  66.   
  67.                     //在这里可以给客户端发送信息哦  
  68.                     channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息")  
  69.                             .getBytes()));  
  70.                     //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。  
  71.                     channel.register(this.selector, SelectionKey.OP_READ);  
  72.   
  73.                     // 获得了可读的事件  
  74.                 } else if (key.isReadable()) {  
  75.                     read(key);  
  76.                 }  
  77.   
  78.             }  
  79.   
  80.         }  
  81.     }  
  82.   
  83.     /** 
  84.      * 处理读取客户端发来的信息 的事件 
  85.      * @param key 
  86.      * @throws IOException  
  87.      */  
  88.     public void read(SelectionKey key) throws IOException {  
  89.         // 服务器可读取消息:得到事件发生的Socket通道  
  90.         SocketChannel channel = (SocketChannel) key.channel();  
  91.         // 创建读取的缓冲区  
  92.         ByteBuffer buffer = ByteBuffer.allocate(1024);  
  93.         channel.read(buffer);  
  94.         byte[] data = buffer.array();  
  95.         String msg = new String(data).trim();  
  96.         System.out.println("服务端收到信息:" + msg);  
  97.         ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
  98.         channel.write(outBuffer);// 将消息回送给客户端  
  99.     }  
  100.   
  101.     /** 
  102.      * 启动服务端测试 
  103.      * @throws IOException  
  104.      */  
  105.     public static void main(String[] args) throws IOException {  
  106.         NIOServer server = new NIOServer();  
  107.         server.initServer(8001);  
  108.         server.listen();  
  109.     }  
  110.   
  111. }  
  112. </span>  
  1. <span style="font-size:18px">package nio;  
  2.   
  3. import java.io.IOException;  
  4. import java.net.InetSocketAddress;  
  5. import java.nio.ByteBuffer;  
  6. import java.nio.channels.SelectionKey;  
  7. import java.nio.channels.Selector;  
  8. import java.nio.channels.SocketChannel;  
  9. import java.util.Iterator;  
  10.   
  11. /** 
  12.  * NIO客户端 
  13.  * @author 小路 
  14.  */  
  15. public class NIOClient {  
  16.     //通道管理器  
  17.     private Selector selector;  
  18.   
  19.     /** 
  20.      * 获得一个Socket通道,并对该通道做一些初始化的工作 
  21.      * @param ip 连接的服务器的ip 
  22.      * @param port  连接的服务器的端口号          
  23.      * @throws IOException 
  24.      */  
  25.     public void initClient(String ip, int port) throws IOException {  
  26.         // 获得一个Socket通道  
  27.         SocketChannel channel = SocketChannel.open();  
  28.         // 设置通道为非阻塞  
  29.         channel.configureBlocking(false);  
  30.         // 获得一个通道管理器  
  31.         this.selector = Selector.open();  
  32.   
  33.         // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调  
  34.         //用channel.finishConnect();才能完成连接  
  35.         channel.connect(new InetSocketAddress(ip, port));  
  36.         //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。  
  37.         channel.register(selector, SelectionKey.OP_CONNECT);  
  38.     }  
  39.   
  40.     /** 
  41.      * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理 
  42.      * @throws IOException 
  43.      */  
  44.     @SuppressWarnings("unchecked")  
  45.     public void listen() throws IOException {  
  46.         // 轮询访问selector  
  47.         while (true) {  
  48.             selector.select();  
  49.             // 获得selector中选中的项的迭代器  
  50.             Iterator ite = this.selector.selectedKeys().iterator();  
  51.             while (ite.hasNext()) {  
  52.                 SelectionKey key = (SelectionKey) ite.next();  
  53.                 // 删除已选的key,以防重复处理  
  54.                 ite.remove();  
  55.                 // 连接事件发生  
  56.                 if (key.isConnectable()) {  
  57.                     SocketChannel channel = (SocketChannel) key.channel();  
  58.                     // 如果正在连接,则完成连接  
  59.                     if (channel.isConnectionPending()) {  
  60.                         channel.finishConnect();  
  61.   
  62.                     }  
  63.                     // 设置成非阻塞  
  64.                     channel.configureBlocking(false);  
  65.   
  66.                     //在这里可以给服务端发送信息哦  
  67.                     channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息")  
  68.                             .getBytes()));  
  69.                     //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。  
  70.                     channel.register(this.selector, SelectionKey.OP_READ);  
  71.   
  72.                     // 获得了可读的事件  
  73.                 } else if (key.isReadable()) {  
  74.                     read(key);  
  75.                 }  
  76.   
  77.             }  
  78.   
  79.         }  
  80.     }  
  81.   
  82.     /** 
  83.      * 处理读取服务端发来的信息 的事件 
  84.      * @param key 
  85.      * @throws IOException  
  86.      */  
  87.     public void read(SelectionKey key) throws IOException {  
  88.         //和服务端的read方法一样  
  89.         // 服务器可读取消息:得到事件发生的Socket通道  
  90.         SocketChannel channel = (SocketChannel) key.channel();  
  91.         // 创建读取的缓冲区  
  92.         ByteBuffer buffer = ByteBuffer.allocate(1024);  
  93.         channel.read(buffer);  
  94.         byte[] data = buffer.array();  
  95.         String msg = new String(data).trim();  
  96.         System.out.println("服务端收到信息:" + msg);  
  97.         ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());  
  98.   
  99.     }  
  100.   
  101.     /** 
  102.      * 启动客户端测试 
  103.      * @throws IOException  
  104.      */  
  105.     public static void main(String[] args) throws IOException {  
  106.         NIOClient client = new NIOClient();  
  107.         client.initClient("localhost"8001);  
  108.         client.listen();  
  109.     }  
  110.   
  111. }  
  112. </span>  

原文链接:http://blog.csdn.net/burningsheep/article/details/12918189