1 Channel 概述

Channel 是一个通道,可以通过它读取和写入数据,它就像水管一样,网络数据通过 Channel 读取和写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是 InputStream 或者 OutputStream 的子类),而且通道可以用于 读、写或者同时用于读写。因为 Channel 是全双工的,所以它可以比流更好地映射底 层操作系统的 API。

NIO中通过channel封装了对数据源的操作,通过channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel 与文件描述符或者 socket 是一一 对应的。Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套 接字)之间有效地传输数据。
channel 接口源码 :

  1. public interface Channel extends Closeable {
  2. /**
  3. * Tells whether or not this channel is open.
  4. *
  5. * @return <tt>true</tt> if, and only if, this channel is open
  6. */
  7. public boolean isOpen();
  8. /**
  9. * Closes this channel.
  10. *
  11. * <p> After a channel is closed, any further attempt to invoke I/O
  12. * operations upon it will cause a {@link ClosedChannelException} to be
  13. * thrown.
  14. *
  15. * <p> If this channel is already closed then invoking this method has no
  16. * effect.
  17. *
  18. * <p> This method may be invoked at any time. If some other thread has
  19. * already invoked it, however, then another invocation will block until
  20. * the first invocation is complete, after which it will return without
  21. * effect. </p>
  22. *
  23. * @throws IOException If an I/O error occurs
  24. */
  25. public void close() throws IOException;
  26. }

与缓冲区不同,通道 API 主要由接口指定。不同的操作系统上通道实现(Channel Implementation)会有根本性的差异,所以通道 API 仅仅描述了可以做什么。因此很 自然地,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移 植的方式来访问底层的 I/O 服务。

Channel是一个对象,可以通过它读取和写入数据。拿 NIO 与原来的 I/O 做个比 较,通道就像是流。所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接 从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。
Java NIO 的通道类似流,但又有些不同:

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

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

    2 Channel 实现

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

  • FileChannel: 从文件中读写数据

  • DatagramChannel: 能通过 UDP 读写网络中的数据
  • SocketChannel: 能通过 TCP 读写网络中的数据
  • ServerSocketChannel: 可以监听新进来的 TCP 连接,像 Web 服务器那样。对 每一个新进来的连接都会创建一个 SocketChannel。

    3 FileChannel 介绍和示例

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

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

    1. public class FileChannelDemo1 {
    2. public static void main(String[] args) throws IOException {
    3. // 创建FileChannel
    4. RandomAccessFile aFile = new RandomAccessFile("d:\\nio.txt", "rw");
    5. FileChannel channel = aFile.getChannel();
    6. // 创建 buffer
    7. ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    8. ///读取到buffer中
    9. int byteRead = channel.read(byteBuffer);
    10. while (byteRead != -1) {
    11. System.out.println("读取:" + byteRead);
    12. byteBuffer.flip();
    13. while (byteBuffer.hasRemaining()) {
    14. System.out.print((char) byteBuffer.get());
    15. }
    16. byteBuffer.clear();
    17. byteRead = channel.read(byteBuffer);
    18. }
    19. channel.close();
    20. System.out.println("finished");
    21. }
    22. }

    Buffer 通常的操作

  • 将数据写入缓冲区

  • 调用buffer.flip() 反转读写模式
  • 从缓冲区读取数据
  • 调用 buffer.clear()或buffer.compact()清除缓冲区内容

    4 FileChannel 操作详解

    4.1 打开 FileChannel

    在使用FileChannel之前,必须先打开它。但是,我们无法直接打开一个 FileChannel,需要通过使用一个 InputStream、OutputStream 或 RandomAccessFile 来获取一个 FileChannel 实例。下面是通过RandomAccessFile打开FileChannel的示例:
    1. RandomAccessFile aFile = new RandomAccessFile("d:\\nio.txt", "rw");
    2. FileChannel inChannel = aFile.getChannel();

    4.2 从 FileChannel 读取数据

    调用多个 read()方法之一从 FileChannel 中读取数据。如:
    1. ByteBuffer buf = ByteBuffer.allocate(48);
    2. int bytesRead = inChannel.read(buf);
    首先,分配一个 Buffer。从 FileChannel 中读取的数据将被读到 Buffer 中。然后,调 用 FileChannel.read()方法。该方法将数据从 FileChannel 读取到 Buffer 中。read() 方法返回的 int 值表示了有多少字节被读到了 Buffer 中。如果返回-1,表示到了文件末尾。

    4.3 向FileChannel写数据

    使用 FileChannel.write()方法向 FileChannel 写数据,该方法的参数是一个 Buffer。 如:
    1. public class FileChannelDemo {
    2. public static void main(String[] args) throws IOException {
    3. RandomAccessFile aFile = new
    4. RandomAccessFile("D:\\nio.txt", "rw");
    5. FileChannel inChannel = aFile.getChannel();
    6. String newData = "New String to write to file..." +
    7. System.currentTimeMillis();
    8. ByteBuffer buf1 = ByteBuffer.allocate(48);
    9. buf1.clear();
    10. buf1.put(newData.getBytes());
    11. buf1.flip();
    12. while(buf1.hasRemaining()) {
    13. inChannel.write(buf1);
    14. }
    15. inChannel.close();
    16. }
    17. }
    注意 FileChannel.write()是在 while 循环中调用的。因为无法保证 write()方法一次能 向 FileChannel 写入多少字节,因此需要重复调用 write()方法,直到 Buffer 中已经没 有尚未写入通道的字节。

4.4 关闭 FileChannel

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

inChannel.close();

4.5 FileChannel的position方法

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

这里有两个例子:

long pos = channel.position();

channel.position(pos +123);

如果将位置设置在文件结束符之后,然后试图从文件通道中读取数据,读方法将返回1 (文件结束标志).如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并 写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙.

4.6 FileChannel的size方法

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

long fileSize = channel.size();

4.7 FileChannel 的 truncate 方法

可以使用 FileChannel.truncate()方法截取一个文件。截取文件时,文件将中指定长度 后面的部分将被删除。如:

channel.truncate(1024);

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

4.8 FileChannel 的 force 方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写到磁盘上。出于性能方 面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到 FileChannel 里的 数据一定会即时写到磁盘上。要保证这一点,需要调用 force()方法。

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

4.9 FileChannel 的 transferTo 和 transferFrom 方法

通道之间的数据传输:
如果两个通道中有一个是 FileChannel,那你可以直接将数据从一个 channel 传输到 另外一个 channel。
(1)transferFrom()方法
FileChannel 的 transferFrom()方法可以将数据从源通道传输到 FileChannel 中(译 者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道 的文件中)。下面是一个 FileChannel完成文件间的复制的demo

  1. public class FileChannelDemo3 {
  2. public static void main(String[] args) throws IOException {
  3. RandomAccessFile aFile = new RandomAccessFile("D:\\nio.txt","rw");
  4. FileChannel aChannel = aFile.getChannel();
  5. RandomAccessFile bFile = new RandomAccessFile("D:\\nio-copy.txt","rw");
  6. FileChannel bFileChannel = bFile.getChannel();
  7. aChannel.transferTo(0,aChannel.size(),bFileChannel);
  8. aChannel.close();
  9. bFileChannel.close();
  10. System.out.println("over");
  11. }
  12. }

方法的输入参数 position 表示从 position 处开始向目标文件写入数据,count 表示 最多传输的字节数。如果源通道的剩余空间小于 count 个字节,则所传输的字节数要 小于请求的字节数。此外要注意,在 SoketChannel 的实现中,SocketChannel 只会 传输此刻准备好的数据(可能不足 count 字节)。因此,SocketChannel 可能不会将 请求的所有数据(count 个字节)全部传输到 FileChannel 中。
(2)transferTo()方法
transferTo()方法将数据从 FileChannel 传输到其他的 channel 中。用法同transferFrom

5.Socket 通道

(1)新的 socket 通道类可以运行非阻塞模式并且是可选择的,可以激活大程序(如 网络服务器和中间件组件)巨大的可伸缩性和灵活性。本节中我们会看到,再也没有 为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换 开销。借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了 并且只有很少甚至可能没有性能损失。所有的 socket 通道类(DatagramChannel、 SocketChannel 和 ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中 的 AbstractSelectableChannel。这意味着我们可以用一个 Selector 对象来执行 socket 通道的就绪选择(readiness selection)。