NIO学习之NIO概述和FileChannel详解

2021-12-07 18:41:11 浏览数 (1)

NIO学习之NIO概述和FileChannel详解

  • 入门
    • 概述
    • 阻塞 IO
    • 非阻塞 IO(NIO)
    • NIO 概述
    • Channel
    • Buffer
    • Selector
    • 通道和缓冲区
  • Java NIO(Channel)
    • Channel 概述
    • channel 接口源码
    • Channel 实现
      • FileChannel 介绍和示例
      • 从 FileChannel 读数据
      • FileChannel 操作详解
        • 打开 FileChannel
        • 从 FileChannel 读取数据
      • 向 FileChannel 写数据
      • 关闭 FileChannel
      • FileChannel 的 position 方法
      • FileChannel 的 size 方法
      • FileChannel 的 truncate 方法
      • FileChannel 的 force 方法
      • FileChannel 的 transferTo 和 transferFrom 方法
        • (1)transferFrom()方法
        • (2)transferTo()方法
      • FileChannel参考文章
      • RandomAccessFile参考文章

入门

概述

Java NIO(New IO 或 Non Blocking IO)是从 Java 1.4 版本开始引入的一个新的IO API,可以替代标准的 Java IO API。NIO 支持面向缓冲区的、基于通道的 IO 操作。NIO 将以更加高效的方式进行文件的读写操作。


阻塞 IO

通常在进行同步 I/O 操作时,如果读取数据,代码会阻塞直至有可供读取的数据。

同样,写入调用将会阻塞直至数据能够写入。

传统的 Server/Client 模式会基于 TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户请求。

这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。

大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有 100 个线程,而有100 个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第101 个用户只想请求一个几 KB 大小的页面。

传统的 Server/Client 模式如下图所示:


非阻塞 IO(NIO)

NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。

NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对 象告诉我们所发生的事件,如下图所示:

从图中可以看出,当有读或写等任何注册的事件发生时,可以从 Selector 中获得相应的 SelectionKey,同时从 SelectionKey 中可以找到发生的事件和该事件所发生的具体的 SelectableChannel,以获得客户端发送过来的数据。

非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的.

区别是阻塞的 IO 会阻塞在 IO 操作上, NIO 阻塞在事件获取上,没有事件就没有 IO, 从高层次看 IO 就不阻塞了.

也就是说只有 IO 已经发生那么我们才评估 IO 是否阻塞,但是select()阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢?

NIO 的本质是延迟 IO 操作到真正发生 IO 的时候,而不是以前的只要 IO 流打开了就一直等待 IO 操作


NIO 概述

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

  • Channels
  • Buffers
  • Selectors

虽然 Java NIO 中除此之外还有很多类和组件,但 Channel,Buffer 和 Selector 构成了核心的 API。

其它组件,如 Pipe 和 FileLock,只不过是与三个核心组件共同使用的工具类。


Channel

首先说一下 Channel,可以翻译成“通道”。

Channel 和 IO 中的 Stream(流)是差不多一个等级的。

只不过 Stream 是单向的,譬如:InputStream, OutputStream.

而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。

NIO 中的 Channel 的主要实现有:FileChannel、DatagramChannel、SocketChannel 和 ServerSocketChannel,这里看名字就可以猜出个所以然来:分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)


Buffer

NIO 中的关键 Buffer 实现有:ByteBuffer, CharBuffer, DoubleBuffer, FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数据类型: byte, char, double, float, int, long, short。


Selector

Selector 运行单线程处理多个 Channel,如果你的应用打开了多个通道,但每个连接的流量都很低,使用 Selector 就会很方便。例如在一个聊天服务器中。要使用Selector, 得向 Selector 注册 Channel,然后调用它的 select()方法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。


通道和缓冲区

Java NIO系统的核心在于:通道(Channel)和缓冲区(Buffer)。

  • 通道表示打开到IO设备(例如:文件、套接字)的连接。
  • 若需要使用NIO系统,需要获取用于连接IO设备的通道以及用于容纳数据的缓冲区。然后操作缓冲区,对数据进行处理。

简而言之,Channel负责传输,Buffer负责存储。


Java NIO(Channel)

Channel 概述

由 java.nio.channels 包定义 的。Channel 表示 IO 源与目标打开的连接。 Channel 类似于传统的“流”。只不过 Channel 本身不能直接访问数据,Channel 只能与 Buffer 进行交互。

通道可以用来读取和写入数据,通道类似于之前的输入/输出流,但是程序不会直接操作通道的,所有的内容都是先读到或写入到缓冲区中,再通过缓冲区中取得获写入的。

通道与传统的流操作不同,传统的流操作是分为输入或输出流的,而通道本身是双向操作的,即可以完成输入也可以完成输出。


channel 接口源码

代码语言:javascript复制
/**
 * A nexus for I/O operations.
 *
 * <p> A channel represents an open connection to an entity such as a hardware
 * device, a file, a network socket, or a program component that is capable of
 * performing one or more distinct I/O operations, for example reading or
 * writing.
 *
 * <p> A channel is either open or closed.  A channel is open upon creation,
 * and once closed it remains closed.  Once a channel is closed, any attempt to
 * invoke an I/O operation upon it will cause a {@link ClosedChannelException}
 * to be thrown.  Whether or not a channel is open may be tested by invoking
 * its {@link #isOpen isOpen} method.
 *
 * <p> Channels are, in general, intended to be safe for multithreaded access
 * as described in the specifications of the interfaces and classes that extend
 * and implement this interface.
 *
 *
 * @author Mark Reinhold
 * @author JSR-51 Expert Group
 * @since 1.4
 */
public interface Channel extends Closeable {

    /**
     * Tells whether or not this channel is open.
     *
     * @return <tt>true</tt> if, and only if, this channel is open
     */
    public boolean isOpen();

    /**
     * Closes this channel.
     *
     * <p> After a channel is closed, any further attempt to invoke I/O
     * operations upon it will cause a {@link ClosedChannelException} to be
     * thrown.
     *
     * <p> If this channel is already closed then invoking this method has no
     * effect.
     *
     * <p> This method may be invoked at any time.  If some other thread has
     * already invoked it, however, then another invocation will block until
     * the first invocation is complete, after which it will return without
     * effect. </p>
     *
     * @throws  IOException  If an I/O error occurs
     */
    public void close() throws IOException;

}

与缓冲区不同,通道 API 主要由接口指定。

不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。

因此很自然地,通道实现经常使用操作系统的本地代码。

通道接口允许您以一种受控且可移植的方式来访问底层的 I/O 服务

Channel 是一个对象,可以通过它读取和写入数据。

拿 NIO 与原来的 I/O 做个比较,通道就像是流。

所有数据都通过 Buffer 对象来处理。

您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。

同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

Java NIO 的通道类似流,但又有些不同:

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

正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:


Channel 实现

下面是 Java NIO 中最重要的 Channel 的实现:

  • FileChannel
  • DatagramChannel
  • SocketChannel
  • ServerSocketChannel

(1)FileChannel 从文件中读写数据。

(2)DatagramChannel 能通过 UDP 读写网络中的数据。

(3)SocketChannel 能通过 TCP 读写网络中的数据。

(4)ServerSocketChannel 可以监听新进来的 TCP 连接,像 Web 服务器那样。对每一个新进来的连接都会创建一个 SocketChannel。

正如你所看到的,这些通道涵盖了 UDP 和 TCP 网络 IO,以及文件 IO


FileChannel 介绍和示例

FileChannel 类可以实现常用的 read,write 以及 scatter/gather 操作,同时它也提供了很多专用于文件的新方法。这些方法中的许多都是我们所熟悉的文件操作。

下面是一个使用 FileChannel 读取数据到 Buffer 中的示例:

Buffer 通常的操作


从 FileChannel 读数据

代码语言:javascript复制
public class FileChannelDemo {
 
 public static void main(String[] args) throws IOException {
//构造一个随机读取文件的类对象---模式设置为读写
  RandomAccessFile aFile = new RandomAccessFile("C:/Users/zdh/Desktop/NIO/a.txt", "rw");
//获取当前随机读写文件的NIO通道
  FileChannel inChannel = aFile.getChannel();
//分配一个大小为48字节的缓冲区对象
  ByteBuffer buf = ByteBuffer.allocate(48);
//读通道中读取数据到缓冲区中
  int bytesRead = inChannel.read(buf);
//bytesRead==-1读取完毕
  while (bytesRead != -1)
  {
   //当前读取的字节数
   System.out.println("读取: "   bytesRead);
  //类似于flush()函数功能,将buffer里面的数据刷新出去
   buf.flip();
 //检查是否还有数据未写入
   while (buf.hasRemaining())
   {
    //打印读到的数据
     System.out.print((char) buf.get());
   }
//清空缓冲区
   buf.clear();
//继续读取数据到缓冲区
   bytesRead = inChannel.read(buf);
  }
//关闭文件流
  aFile.close();

  System.out.println("操作结束");

 }

}

将数据写入缓冲区

调用 buffer.flip() 反转读写模式

从缓冲区读取数据

调用 buffer.clear() 或 buffer.compact() 清除缓冲区内容


FileChannel 操作详解

打开 FileChannel

在使用 FileChannel 之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个 InputStream、OutputStream 或 RandomAccessFile 来获取一个 FileChannel 实例。

下面是通过 RandomAccessFile打开 FileChannel 的示例:

代码语言:javascript复制
RandomAccessFile aFile = new RandomAccessFile("d:\atguigu\01.txt", 
"rw");
FileChannel inChannel = aFile.getChannel();
从 FileChannel 读取数据

调用多个 read()方法之一从 FileChannel 中读取数据。如:

代码语言:javascript复制
ByteBuffer buf = ByteBuffer.allocate(48);
int bytesRead = inChannel.read(buf);

首先,分配一个 Buffer。

从 FileChannel 中读取的数据将被读到 Buffer 中。

然后,调用 FileChannel.read()方法。

该方法将数据从 FileChannel 读取到 Buffer 中。

read()方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾。


向 FileChannel 写数据

使用 FileChannel.write()方法向 FileChannel 写数据,该方法的参数是一个 Buffer。如:

代码语言:javascript复制
public class FileChannelDemo {
 
 public static void main(String[] args) throws IOException {
//构造一个随机读取文件的类对象---模式设置为读写
  RandomAccessFile aFile = new RandomAccessFile("C:/Users/zdh/Desktop/NIO/a.txt", "rw");
//获取当前随机读写文件的NIO通道
  FileChannel inChannel = aFile.getChannel();
  //准备向文件中写入的数据
  String newData = "New String to write to file..."  
          System.currentTimeMillis();
  //分配缓冲区--大小为48字节
  ByteBuffer buf1 = ByteBuffer.allocate(48);
  //清除缓冲区
  buf1.clear();
  //缓冲区中放入数据
  buf1.put(newData.getBytes());
  //反转读写模式
  buf1.flip();
//缓冲区中还有数据
  while(buf1.hasRemaining())
  {
   //往通道中写入数据
   inChannel.write(buf1);
  }
  //通道关闭
  inChannel.close();
 }

}

注意 FileChannel.write()是在 while 循环中调用的。因为无法保证 write()方法一次能向 FileChannel 写入多少字节,因此需要重复调用 write()方法,直到 Buffer 中已经没有尚未写入通道的字节。


关闭 FileChannel

用完 FileChannel 后必须将其关闭。如:

代码语言:javascript复制
inChannel.close();

FileChannel 的 position 方法

有时可能需要在 FileChannel 的某个特定位置进行数据的读/写操作。可以通过调用position()方法获取 FileChannel 的当前位置。也可以通过调用 position(long pos)方法设置 FileChannel 的当前位置。

这里有两个例子

long pos = channel.position(); channel.position(pos 123);:

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回-1 (文件结束标志)。

如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。

这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。


FileChannel 的 size 方法

FileChannel 实例的 size()方法将返回该实例所关联文件的大小。如:

代码语言:javascript复制
long fileSize = channel.size();

FileChannel 的 truncate 方法

可以使用 FileChannel.truncate()方法截取一个文件。

截取文件时,文件将中指定长度后面的部分将被删除。如:

代码语言:javascript复制
channel.truncate(1024);

这个例子截取文件的前 1024 个字节。


FileChannel 的 force 方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。

出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的 数据一定会即时写到磁盘上。

要保证这一点,需要调用 force()方法。

force()方法有一个 boolean 类型的参数,指明是否同时将文件元数据(权限信息等)写到磁盘上


FileChannel 的 transferTo 和 transferFrom 方法

通道之间的数据传输:

如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到另外一个 channel。


(1)transferFrom()方法

FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。

下面是一个 FileChannel 完成文件间的复制的例子:

代码语言:javascript复制
public class FileChannelDemo {

 public static void main(String args[]) throws Exception {
  String src = System.getProperty("user.dir")   System.getProperty("file.separator");
  System.out.println("当前工作目录: " src);
  //待复制的文件
  RandomAccessFile aFile = new
          RandomAccessFile
          (src "origin.txt", "rw");
  //获取到待复制文件的通道
  FileChannel fromChannel = aFile.getChannel();
 //目的文件
  RandomAccessFile bFile = new
          RandomAccessFile
          (src "copy.txt", "rw");
 //目的文件的通道
  FileChannel toChannel = bFile.getChannel();
  //开始的位置
  long position = 0;
  //复制的总字节数量
  long count = fromChannel.size();
  //从待复制文件的通过获取数据,开始位置,复制的大小
  toChannel.transferFrom(fromChannel, position, count);
  //先关闭待复制文件通道
  aFile.close();
  //再关闭目的文件的通道
  bFile.close();
  System.out.println("over!");
 }

}

方法的输入参数 position 表示从 position 处开始向目标文件写入数据,count 表示最多传输的字节数。

如果源通道的剩余空间小于 count 个字节,则所传输的字节数要小于请求的字节数。

此外要注意,在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节)。

因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中


(2)transferTo()方法

transferTo()方法将数据从 FileChannel 传输到其他的 channel 中。

下面是一个 transferTo()方法的例子:

代码语言:javascript复制
public class FileChannelDemo {

 public static void main(String args[]) throws Exception {
  String src = System.getProperty("user.dir")   System.getProperty("file.separator");
  System.out.println("当前工作目录: " src);
  //待复制的文件
  RandomAccessFile aFile = new
          RandomAccessFile
          (src "origin.txt", "rw");
  //获取到待复制文件的通道
  FileChannel fromChannel = aFile.getChannel();
 //目的文件
  RandomAccessFile bFile = new
          RandomAccessFile
          (src "copy.txt", "rw");
 //目的文件的通道
  FileChannel toChannel = bFile.getChannel();
  //开始的位置
  long position = 0;
  //复制的总字节数量
  long count = fromChannel.size();
  //起始位置,总字节数,目的地
  fromChannel.transferTo(position, count, toChannel);
  //先关闭待复制文件通道
  aFile.close();
  //再关闭目的文件的通道
  bFile.close();
  System.out.println("over!");

 }

}

FileChannel参考文章

Java之FileChannel类的理解和使用 Java之FileChannel类的理解和使用 -----java 流NIO的使用

Java NIO学习篇之通道FileChannel详解


RandomAccessFile参考文章

RandomAccessFile类使用详解

0 人点赞