关于NIO

来自Wikioe
Eijux讨论 | 贡献2021年1月6日 (三) 02:40的版本 →‎Buffer
跳到导航 跳到搜索


关于

Java NIO,即“New I/O”(另一说“No-Blocking I/O”):采用内存映射文件的方式来处理输入输出:NIO将文件或文件的一段区域映射到内存中,这样就可以像访问内存一样访问文件了。

与 IO

NIO与标准IO不同:NIO支持面向缓冲区的、基于通道的IO操作,以更加高效的方式进行文件的读写操作。

  • (“旧”的I/O包已经使用NIO重新实现过,“即使我们不显式的使用NIO编程,也能从中受益”)
  • (NIO子系统不会取代java.io包中可用的基于流的I/O类)
IO NIO
面向流(Stream Oritented) 面向缓冲区(Buffer Oritented)
阻塞IO(Blocking IO) 非阻塞IO(None Blocking IO)
选择器(Selecters)

NIO 包

从JDK1.4开始提供的一系列改进的输入/输出处理,这些类都被放在java.nio包及子包下:

Java.nio相关包.png
包名称 使用/目的
java.nio NIO系统的顶级包,NIO系统封装了各种类型的缓冲区。
java.nio.charset 封装了字符集,并且还支持分别将字符转换为字节和字节到编码器和解码器的操作。
java.nio.charset.spi 用于支持字符集服务提供者
java.nio.channels 支持通道,这些通道本质上是打开I/O连接。
java.nio.channels.spi 用于支持频道的服务提供者
java.nio.file 提供对文件的支持
java.nio.file.spi 用于支持文件系统的服务提供者
java.nio.file.attribute 用于提供对文件属性的支持

(为什么学习NIO)

IO操作往往在两个场景下会用到:

  1. 文件I/O
  2. 网络I/O:NIO的优势体现;

【IO:同步、异步,阻塞、非阻塞】

(见IO - 同步,异步,阻塞,非阻塞 (亡羊补牢篇)

NIO 组件

NIO中的所有I/O都是通过一个通道开始的。数据总是从缓冲区写入通道,并从通道读取到缓冲区:

  1. 从通道读取:创建一个缓冲区,然后请求通道读取数据;
    NIO:数据读取.png
  2. 通道写入:创建一个缓冲区,填充数据,并要求通道写入数据;
    NIO:数据写入.png

Java NIO 由以下几个核心部分组成:

  • Buffer
  • Channel
  • Selector

Channel

通道,是用于在实体和字节缓冲区之间有效传输数据的介质。它从一个实体读取数据,并将其放在缓冲区块中以供消费。 Java NIO Channel 不同于流:

  1. 既可以从通道中读取数据,又可以写数据到通道;但流的读写通常是单向的。
  2. 通道可以异步地读写。
  3. 通道中的数据总是要先读到一个Buffer,或者总是要从一个Buffer中写入。


“java.nio.channels”类的层次结构:

Java.nio.channels类的层次结构.png

其中顶层接口:

package java.nio.channels;

import java.io.IOException;
import java.io.Closeable;

public interface Channel extends Closeable {
    // 检查通道是否打开
    public boolean isOpen();
    // 关闭通道
    public void close() throws IOException;

}

Java NIO中,主要使用的通道如下:

  1. FileChannel”:从文件中读写数据;
  2. DatagramChannel”:通过UDP读写网络中的数据;
  3. SocketChannel”:通过TCP读写网络中的数据;
  4. ServerSocketChannel”:监听新进来的TCP连接,像Web服务器那样。对每一个新进来的连接都会创建一个SocketChannel;


示例:

RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf);
while (bytesRead != -1) 
{
   System.out.println("Read " + bytesRead);
   buf.flip();

   while(buf.hasRemaining())
   {
      System.out.print((char) buf.get());
   }
   buf.clear();
   bytesRead = inChannel.read(buf);
}
aFile.close();

Buffer

Java NIO中的Buffer用于和NIO通道进行交互:

缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存。这块内存被包装成NIO Buffer对象,并提供了一组方法,用来方便的访问该块内存。

在Java NIO中使用的核心缓冲区如下:

  • ByteBuffer
  • MappedByteBuffer
  • CharBuffer
  • DoubleBuffer
  • FloatBuffer
  • IntBuffer
  • LongBuffer
  • ShortBuffer

Java.nio.Buffer类结构.png

capacity、position和limit

缓冲区本质上是一块“可以写入数据,然后可以从中读取数据”的内存:这块内存被包装成“NIO Buffer”对象,并提供了一组方法,用来方便的访问该块内存。
为了理解Buffer的工作原理,需要熟悉它的三个属性:

  1. capacity:容量;
  2. position:读写位置,即下一个值将在此进行读写;
    1. 写模式:初始的position值为0,最大可为capacity-1;
    2. 读模式:当将Buffer从写模式切换到读模式,position会被重置为0;
  3. limit:界限,即超过它进行读写是没有意义的;
    1. 写模式:limit等于Buffer的capacity。
    2. 读模式:当切换Buffer到读模式时,limit会被设置成写模式下的position值(已写入数据位置);
  • 不管Buffer处在什么模式,“capacity”的含义总是一样的;
Capacity、position和limit.png

基本用法

使用Buffer读写数据,遵循以下四个步骤:

  1. 写入数据到Buffer;   // 向buffer写入数据时,buffer会记录下写了多少数据
  2. 调用“flip()”方法;   // 通过flip()方法将Buffer从写模式切换到读模式
  3. 从Buffer中读取数据;   // 读模式下,可以读取之前写入到buffer的所有数据
  4. 调用“clear()”方法或者“compact()”方法;   // 读完所有数据,就清空缓冲区让它可以再次被写入:clear()方法会清空整个缓冲区;compact()方法只会清除已经读过的数据
RandomAccessFile aFile = new RandomAccessFile("data/nio-data.txt", "rw");
FileChannel inChannel = aFile.getChannel();

//create buffer with capacity of 48 bytes
ByteBuffer buf = ByteBuffer.allocate(48);

int bytesRead = inChannel.read(buf); //read into buffer.
while (bytesRead != -1) {
   buf.flip();  //make buffer ready for read
   while(buf.hasRemaining())
   {
      System.out.print((char) buf.get()); // read 1 byte at a time
   }
   buf.clear(); //make buffer ready for writing
   bytesRead = inChannel.read(buf);
}
aFile.close();
  1. Buffer的分配:
    // 分配48字节capacity的ByteBuffer
    ByteBuffer buf = ByteBuffer.allocate(48);
    // 分配一个可存储1024个字符的CharBuffer
    CharBuffer buf = CharBuffer.allocate(1024);
    
  2. 向Buffer中写数据:
    1. 从Channel写到Buffer:
      int bytesRead = inChannel.read(buf); //read into buffer.
      
    2. 通过Buffer的“put()”方法写到Buffer里:
      buf.put(127);
      
      • put()”方法有很多版本,允许以不同的方式把数据写入到Buffer中;
  3. 从Buffer中读取数据:
    1. 从Buffer读取数据到Channel:
      int bytesWritten = inChannel.write(buf); // read from buffer into channel.
      
    2. 使用“get()”方法从Buffer中读取数据:
      byte aByte = buf.get();
      
      • “get”方法有很多版本,允许以不同的方式从Buffer中读取数据;

常用方法

  • flip()”:准备读入:
    即,将“position”设回0,并将“limit”设置成之前“position”的值。
  • rewind()”:准备重新读入:
    即,将“position”设回0,所以你可以重读Buffer中的所有数据。“limit”保持不变,仍然表示能从Buffer中读取多少个元素(byte、char等)。
  • clear()”与“compact()”:准备写入:
    1. “clear()”:“position”将被设回0,“limit”被设置成 capacity的值。(Buffer被清空了,但Buffer中的数据并未清除,只是这些标记告诉我们可以从哪里开始往Buffer里写数据)
      • Buffer中未读数据将“被遗忘”。
    2. “compact()”:将所有未读的数据拷贝到Buffer起始处。然后将position设到最后一个未读元素正后面。limit属性依然像clear()方法一样,设置成capacity。
      • 不会覆盖未读的数据。
  • mark()”与“reset()”:
    通过调用Buffer.mark()方法,可以标记Buffer中的一个特定position。之后可以通过调用Buffer.reset()方法恢复到这个position。
  • equals()”与“compareTo()”:比较两个Buffer:(剩余元素是从 position到limit之间的元素)
    1. “equals()”:当满足下列条件时,表示两个Buffer相等:(只是比较Buffer的一部分,不是每一个在它里面的元素都比较。实际上,它只比较Buffer中的剩余元素)
      1. 有相同的类型(byte、char、int等)。
      2. Buffer中剩余的byte、char等的个数相等。
      3. Buffer中所有剩余的byte、char等都相同。
    2. “compareTo()”:比较两个Buffer的剩余元素(byte、char等), 如果满足下列条件,则认为一个Buffer“小于”另一个Buffer:
      1. 第一个不相等的元素小于另一个Buffer中对应的元素。
      2. 所有元素都相等,但第一个Buffer比另一个先耗尽(第一个Buffer的元素个数比另一个少)。

Selector

Scatter / Gather

分散(scatter) 聚集(gather)

Channel to Channel Transfers

常用Channel

FileChannel DatagramChannel SocketChannel ServerSocketChannel


Java NIO非阻塞式服务器

其他

Pipe Path Files AsynchronousFileChannel