四、JAVA NIO (Channel)

本文深入探讨Java NIO中的Channel,讲解其双向特性与流的区别,并通过FileChannel实例展示读写、关闭、定位、截取等操作。此外,还介绍了Scatter/Gather技术在数据传输中的应用,包括ScatteringReads和GatheringWrites,帮助理解如何在Channel之间高效地分散和聚集数据。

NIO 目录



四、JAVA NIO (Channel)

1、Channel 概述

NIO 通过Channel(通道) 进行读写。

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

  • Channel中的数据总是要先读到一个 Buffer,或者总是要从一个 Buffer 中写入。所有数据都通过 Buffer 对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样,您不会直接从通道中读取字节,而是将数据从通道读入缓冲区,再从缓冲区获取这个字节。

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

  • 既可以从通道中读取数据,又可以写数据到通道。但流的读写通常是单向的。
  • 通道可以异步的读写。
  • 正如上面所说,从通道读取数据到缓冲区,从缓冲区写入数据到通道。如下图所示:

3-5

2、Channel 实现

NIO 中通过 channel 封装了对数据源的操作,通过 channel 我们可以操作数据源,但又不必关心数据源的具体物理结构。这个数据源可能是多种的。比如,可以是文件,也可以是网络 socket。在大多数应用中,channel 与文件描述符或者 socket 是一一对应的。

Channel 用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。

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

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

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

3、FileChannel 介绍和示例

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

4-1

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

package com.wlw.nio.channel;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * FileChannel读取数据到buffer中
 */
public class FileChannelDemo1 {
    public static void main(String[] args) throws IOException {
        // 对这个文件进行操作
        RandomAccessFile aFile =
                new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\01.txt", "rw");
        // 创建FileChannel,没有方法可以直接创建,我们要通过文件来创建
        FileChannel fileChannel = aFile.getChannel();
        // 创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 读取数据到buffer中,返回值为-1 说明读完了
        int bytesRead = fileChannel.read(buffer);
        while (bytesRead != -1) {
            System.out.println("读取: " + bytesRead);
            // 转换读写模式
            buffer.flip();
            // 是否有剩余内容
            while (buffer.hasRemaining()) {
                // 拿到buffer里的内容
                System.out.println((char) buffer.get());
            }
            // 清空buffer
            buffer.clear();
            bytesRead = fileChannel.read(buffer);
        }
        aFile.close();
        System.out.println("操作结束");
    }
}

这里先说下 Buffer 通常的操作(后面有详解):

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

4、FileChannel 操作详解

4.1、打开 FileChannel

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

RandomAccessFile aFile =
    new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\01.txt", "rw");
FileChannel fileChannel = aFile.getChannel();

4.2、从 FileChannel 读取数据

调用多个 read()方法,从 FileChannel 中读取数据到 Buffer 中。如:

ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buffer);

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

4.3、向 FileChannel 写数据

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

package com.wlw.nio.channel;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 向 FileChannel 写数据
 */
public class FileChannelDemo2 {
    public static void main(String[] args) throws IOException {
        // 创建(打开)FileChannel
        RandomAccessFile aFile =
                new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\01.txt", "rw");
        FileChannel fileChannel = aFile.getChannel();

        // 创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        String newData = "New String to write to file..."+System.currentTimeMillis();
        buffer.clear();
        // 写入内容
        buffer.put(newData.getBytes());
        // 转换读写模式,转成读
        buffer.flip();
        while(buffer.hasRemaining()) {
            // 向  FileChannel 写数据
            fileChannel.write(buffer);
        }
        // 关闭通道
        fileChannel.close();
    }
}

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

4.4、关闭 FileChannel

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

fileChannel.close();

4.5、FileChannel 的 position 方法

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

// 获取 FileChannel 的当前位置
long pos = channel.position();

// 设置 FileChannel 的当前位置
channel.position(pos + 123);

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

如果将位置设置在文件结束符之后,然后向通道中写数据,文件将撑大到当前位置并写入数据。这可能导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

4.6、FileChannel 的 size 方法

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

long fileSize = channel.size();

4.7、FileChannel 的 truncate 方法

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

// 截取文件的前 1024 个字节
channel.truncate(1024);

4.8、FileChannel 的 force 方法

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

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

4.9、FileChannel 的transferFrom 和 transferTo方法

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

(1)transferFrom()方法

FileChannel 的 transferFrom() 方法可以将数据从源通道传输到 FileChannel 中(译者注:这个方法在 JDK 文档中的解释为将字节从给定的可读取字节通道传输到此通道的文件中)。下面是一个 FileChannel 完成文件间的复制的例子:

package com.wlw.nio.channel;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

/**
 * 通道之前数据传输
 * transferFrom()方法
 */
public class FileChannelDemo3 {
    public static void main(String[] args) throws IOException {
        // 创建(打开)两个FileChannel
        RandomAccessFile aFile =
                new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\01.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();

        RandomAccessFile bFile =
                new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\02.txt", "rw");
        FileChannel toChannel = bFile.getChannel();

        // 把fromChannel通道里的数据传到toChannel里
        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()方法的例子:

package com.wlw.nio.channel;

import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.channels.FileChannel;

/**
 * 通道之前数据传输
 * transferTo()方法
 */
public class FileChannelDemo4 {
    public static void main(String[] args) throws IOException {
        // 创建(打开)两个FileChannel
        RandomAccessFile aFile =
                new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\01.txt", "rw");
        FileChannel fromChannel = aFile.getChannel();

        RandomAccessFile bFile =
                new RandomAccessFile("D:\\Program Files\\IDEAworkspace4\\IOFiles\\03.txt", "rw");
        FileChannel toChannel = bFile.getChannel();

        // 把fromChannel通道里的数据传到toChannel里
        long position = 0;
        long count = fromChannel.size();
        fromChannel.transferTo( position, count, toChannel);

        // 关闭文件
        aFile.close();
        bFile.close();
        System.out.println("over!");
    }
}

5、Scatter/Gather(分散/聚集)

Java NIO 开始支持 scatter/gather,scatter/gather 用于描述从 Channel 中读取或者写入到 Channel 的操作。

  • 分散(scatter)从 Channel 中读取是指:在读操作时将读取的数据写入多个 buffer 中。因此,Channel 将从 Channel 中读取的数据“分散(scatter)”到多个 Buffer 中。
  • 聚集(gather)写入 Channel 是指:在写操作时将多个 buffer 的数据写入同一个Channel,因此,Channel 将多个 Buffer 中的数据“聚集(gather)”后发送到Channel。

scatter / gather 经常用于需要将传输的数据分开处理的场合,例如传输一个由消息头和消息体组成的消息,你可能会将消息体和消息头分散到不同的 buffer 中,这样你可以方便的处理消息头和消息体。

5.1、Scattering Reads

Scattering Reads 是指数据从一个 channel 读取到多个 buffer 中。如下图描述:

4-2

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);

ByteBuffer[] bufferArray = { header, body };

channel.read(bufferArray);

注意 buffer 首先被插入到数组,然后再将数组作为 channel.read() 的输入参数。read()方法按照 buffer 在数组中的顺序将从 channel 中读取的数据写入到 buffer,当一个 buffer 被写满后,channel 紧接着向另一个 buffer 中写。

Scattering Reads 在移动下一个 buffer 前,必须填满当前的 buffer,这也意味着它不适用于动态消息(译者注:消息大小不固定)。换句话说,如果存在消息头和消息体,消息头必须完成填充(例如 128byte),Scattering Reads 才能正常工作。

5.2、Gathering Writes

Gathering Writes 是指数据从多个 buffer 写入到同一个 channel。如下图描述:

4-3

ByteBuffer header = ByteBuffer.allocate(128);
ByteBuffer body = ByteBuffer.allocate(1024);

// write data into buffers
ByteBuffer[] bufferArray = { header, body };

channel.write(bufferArray);

buffers 数组是 write()方法的入参,write()方法会按照 buffer 在数组中的顺序,将数据写入到 channel,注意只有 position 和 limit 之间的数据才会被写入。因此,如果一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,那么这 58byte 的数据将被写入到 channel 中。因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值