NIO学习笔记

3次阅读

共计 3147 个字符,预计需要花费 8 分钟才能阅读完成。

一、NIO 的核心部分

  1. 通道(channel):用于读操作和写操作,主要负责数据的“运输”。相当于传统 IO 中的 stream,不过 stream 是单向的。
  2. 缓冲区(buffer):一个容器,用于存储数据。
  3. 选择器(selector):核心类,能够检测多个注册的通道上是否有事件发生,因此一个线程可以管理多个通道。

二、为什么使用 NIO

使用 NIO 主要就是为了提高 IO 的速度

  1. 传统 IO 是基于 字节流 字符 进行操作的,而 NIO 是基于 通道(channel) 缓冲区(buffer)进行操作的,这样一来数据的偏移操作非常方便。
  2. 传统 IO 是阻塞的,如果发出 IO 请求后数据没有就绪,会处于阻塞状态,而 NIO 通过 通道 操作数据,因此一个通道无数据可读,可以切换到另一个通道。
  3. NIO 有 选择器,因此单线程可以操作多个通道。

三、简单的 NIO 读和写例子

  1. 从文件中读取
    与传统 IO 不同的是,使用 NIO 从文件中读取主要分为三步:
    (1)从 FileInputStream 中获取 channel;

    FileInputStream fin = new FileInputStream("readandshow.txt");
    FileChannel fc = fin.getChannel();
    

    (2)创建 buffer;

    ByteBuffer buffer = ByteBuffer.allocate(1024);
    

    (3)将数据从 channel 读到 buffer 中。

    fc.read(buffer);
    
    
  2. 写入文件
    类似的,利用 NIO 写入文件也分为三步:
    (1)从 FileInputStream 中获取 channel;

       FileOutputStream fout = new FileOutputStream("writesomebytes.txt");
       FileChannel fc = fout.getChannel();
       

    (2)创建一个缓冲区并在其中放入一些数据;

       ByteBuffer buffer = ByteBuffer.allocate(1024);
       for (int i=0; i<message.length; ++i) {buffer.put( message[i] );  // 从 message 数组中取出放入 buffer
       }
       buffer.flip();    // 将 buffer 由写模式切换为读模式

    (3)将数据从 buffer 写到通道中。

       fc.write(buffer);
       
    

四、buffer 内部细节

  1. flip()函数源码
public final Buffer flip() {
    limit = position;
    position = 0;
    mark = -1;
    return this;
}
 

这里可以注意到 limit,position 和 mark 三个变量
(1)capacity:表明可以储存在缓冲区中的最大数据容量。
(2)position:下一个可插入的位置(写 buffer 时) 或者下一个可读的位置 (读 buffer 时)。
(3)limit:最多能写多少数据(写 buffer 时,相当于 capacity),可以读多少数据(在从通道读入缓冲区时)。
(4)mark:标记,记录当前 position 的位置,可以通过 reset() 恢复到 mark 的位置。

五、选择器

  1. selector 的创建

    Selector selector = Selector.open();

  2. 因为选择器是用于管理通道的,因此需要将通道注册到相应的选择器上
    channel.configureBlocking(false); // 使用 selector 必须保证 channel 是非阻塞的模式
    SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

    注意 register 的第二个参数表示对选择器对什么事件感兴趣。
    返回值 记录了包括 channel,buffer,interest 集合和 ready 集合等。

  3. 通过 selector 选择通道
    可以使用select() 函数进行选择,select()方法返回的 int 值表示有多少通道已经就绪。
  4. selectedKeys()函数
    一旦调用了select() 方法,并且返回值表明有一个或更多个通道就绪了,然后可以通过调用 selector 的 selectedKeys() 方法,访问“已选择键集(selected key set)”中的就绪通道。这里的 selectorKey 的就是之前注册到该 selector 的通道。
  5. 示例程序

    Selector selector = Selector.open();   // 开启选择器
    channel.configureBlocking(false);      // 非阻塞模式
    SelectionKey key = channel.register(selector, SelectionKey.OP_READ);// 注册
    while(true) {int readyChannels = selector.select();   // 选择通道
      if(readyChannels == 0) continue;
      Set selectedKeys = selector.selectedKeys();   // 获取通道
      Iterator keyIterator = selectedKeys.iterator();
      while(keyIterator.hasNext()) {SelectionKey key = keyIterator.next();
        if(key.isAcceptable()) {// a connection was accepted by a ServerSocketChannel.} else if (key.isConnectable()) {// a connection was established with a remote server.} else if (key.isReadable()) {// a channel is ready for reading} else if (key.isWritable()) {// a channel is ready for writing}
        keyIterator.remove();   // 需要自己移除处理完的通道}
    }

六、NIO 和 IO 的使用场景

NIO 具备一定的优点,但并不是说传统 IO 就一无是处

(1)在 处理数据 上,如果遇到逐行处理的情况,如:

    Name: Anna
    Age: 25
    Email: anna@mailserver.com
    Phone: 1234567890

传统的 IO 可以这样写:

BufferedReader reader = new BufferedReader(new InputStreamReader(input));
String nameLine   = reader.readLine();
String ageLine    = reader.readLine();
String emailLine  = reader.readLine();
String phoneLine  = reader.readLine();

但如果使用 NIO:

    ByteBuffer buffer = ByteBuffer.allocate(48);
    int bytesRead = inChannel.read(buffer);

你就不知道缓冲区内是否刚好是一行数据,这样处理起来会比较麻烦。

(2)如果需要管理同时打开的成千上万个连接,这些连接每次只是发送少量的数据,例如聊天服务器,实现 NIO 的服务器可能是一个优势。
如果你需要维持许多打开的连接到其他计算机上,如 P2P 网络中,使用一个单独的线程来管理你所有出站连接,可能是一个优势。
但如果你有少量的连接使用非常高的带宽,一次发送大量的数据,也许典型的 IO 服务器实现可能非常契合。

学习参考:http://ifeve.com/java-nio-all/ Java NIO 系列教程
https://www.ibm.com/developer… NIO 入门

正文完
 0