NIO学习总结(一)——简介、Channel、Buffer

相关代码地址:nio_demo_learn: nio学习相关代码 (gitee.com)

一、BIO、NIO和AIO

1.1 阻塞IO(BIO)

  • BIO即同步阻塞IO,实现模型为一个连接就需要一个线程去处理。这种方式简单来说就是当有客户端来请求服务器时,服务器就会开启一个线程去处理这个请求,即使这个请求不干任何事情,这个线程都一直处于阻塞状态。

  • 这种模式带来的一个问题就是线程数量的剧增,大量的线程会增大服务器的开销。大多数的实现为了避免这个问题,都采用了线程池模型,并设置线程池线程的最大数量,这由带来了新的问题,如果线程池中有 100 个线程,而有100 个用户都在进行大文件下载,会导致第 101 个用户的请求无法及时处理,即便第101 个用户只想请求一个几 KB 大小的页面。传统的 Server/Client 模式如下图所示:

    image-20231217174846833

1.2 非阻塞IO(NIO)

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

  • NIO 中非阻塞 I/O 采用了基于 Reactor 模式的工作方式,I/O 调用不会被阻塞,相反是注册感兴趣的特定 I/O 事件,如可读数据到达,新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO 中实现非阻塞 I/O 的核心对象就是 Selector,Selector 就是注册各种 I/O 事件地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件,如下图所示:

    image-20240225122118591

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

  • 非阻塞指的是 IO 事件本身不阻塞,但是获取 IO 事件的 select()方法是需要阻塞等待的。区别是阻塞的 IO 会阻塞在 IO 操作上,NIO 阻塞在事件获取上,没有事件就没有 IO, 从高层次看 IO 就不阻塞了。也就是说只有 IO 已经发生,那么我们才评估 IO 是否阻塞,但是 select() 阻塞的时候 IO 还没有发生,何谈 IO 的阻塞呢?

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

    IONIO
    面向流(Stream Oriented)面向缓冲区(Buffer Oriented)
    阻塞IO(Blocking IO)非阻塞IO(Non Blocking IO)
    选择器(Selectors)

1.3 异步非阻塞IO(AIO)

  • AIO是在JDK1.7中推出的新的IO方式——异步非阻塞IO,也被称为NIO2.0,AIO在进行读写操作时,直接调用API的read和write方法即可,这两种均是异步的方法,且完成后会主动调用回调函数。简单来讲,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
  • Java提供了四个异步通道:AsynchronousSocketChannel、AsynchronousServerSocketChannel、AsynchronousFileChannel、AsynchronousDatagramChannel。

二、NIO概述

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

  • Channel(通道)
  • Buffer(缓冲区),
  • Selector(多路复用选择器)

虽然 Java NIO 中除此之外还有很多类和组件,但 Channel,Buffer 和 Selector 构成了核心的 API。其它组件,如 Pipe 和 FileLock,只不过是与三个核心组件共同使用的工具类。

2.1 Channel

首先说一下 Channel,可以翻译成“通道”。Channel 和 IO 中的 Stream(流)是差不多一个等级的。只不过 Stream 是单向的,譬如:InputStream, OutputStream。而Channel 是双向的,既可以用来进行读操作,又可以用来进行写操作。

NIO 中的 Channel 的主要实现有:FileChannelDatagramChannelSocketChannelServerSocketChannel,分别可以对应文件 IO、UDP 和 TCP(Server 和 Client)。

2.2 Buffer

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

NIO中Buffer对应的基本数据类型
ByteBufferbyte
CharBufferchar
DoubleBufferdouble
FloatBufferfloat
IntBufferint
LongBufferlong
ShortBuffershort

2.3 Selector

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

三、Channel

3.1 Channel概述

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

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

    image-20240225133855886

  • Channel接口源码:

    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,或者总是要从一个 Buffer 中写入。

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

    image-20240225134208228

3.2 Channel的主要实现

Java NIO 中最重要的 Channel 的实现:

  • FileChannel。从文件中读写数据

  • DatagramChannel。通过 UDP 读写网络中的数据

  • SocketChannel。通过 TCP 读写网络中的数据

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

3.2.1 IO通道——FileChannel

3.2.1.1 介绍和示例

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

public class FileChannelDemo1 {

    /**
     * FileChanel读取数据到buffer中
     * @param args
     * @throws IOException
     */
    public static void main(String[] args) throws IOException {
        // 创建FileChannel
        RandomAccessFile file = new RandomAccessFile("./src/main/resources/01.txt", "rw");
        FileChannel channel = file.getChannel();

        // 创建Buffer
        ByteBuffer buf = ByteBuffer.allocate(1024);

        // 把数据读入Buffer
        int bytesRead = channel.read(buf);
        while (bytesRead != -1) {
            System.out.println("\n读取了:"+bytesRead);
            // 读写切换
            buf.flip();
            // 如果buf中有剩余数据就一直输出
            while(buf.hasRemaining()){
                System.out.print((char)buf.get());
            }
            buf.clear();
            // 继续下次读取
            bytesRead = channel.read(buf);
        }
        // file.close();
        // channel关闭后,file也会关闭
        channel.close();
        System.out.println("\n操作结束!");
    }
}

FileChannel相关方法:

Modifier and TypeMethod and Description
abstract voidforce(boolean metaData)强制将此通道文件的任何更新写入包含该通道的存储设备。
FileLocklock()获取此通道文件的排他锁。
abstract FileLocklock(long position, long size, boolean shared)获取此通道文件的给定区域的锁定。
abstract MappedByteBuffermap(FileChannel.MapMode mode, long position, long size)将此频道文件的区域直接映射到内存中。
static FileChannelopen(Path path, OpenOption... options)打开或创建文件,返回文件通道以访问该文件。
static FileChannelopen(Path path, Set<? extends OpenOption> options, FileAttribute<?>... attrs)打开或创建文件,返回文件通道以访问该文件。
abstract longposition()返回此频道的文件位置。
abstract FileChannelposition(long newPosition)设置此通道的文件位置。
abstract intread(ByteBuffer dst)从该通道读取到给定缓冲区的字节序列。
longread(ByteBuffer[] dsts)从该通道读取到给定缓冲区的字节序列。
abstract longread(ByteBuffer[] dsts, int offset, int length)从该通道读取字节序列到给定缓冲区的子序列中。
abstract intread(ByteBuffer dst, long position)从给定的文件位置开始,从该通道读取一个字节序列到给定的缓冲区。
abstract longsize()返回此通道文件的当前大小。
abstract longtransferFrom(ReadableByteChannel src, long position, long count)从给定的可读字节通道将字节传输到该通道的文件中。
abstract longtransferTo(long position, long count, WritableByteChannel target)将该通道文件的字节传输到给定的可写字节通道。
abstract FileChanneltruncate(long size)将此频道的文件截断为给定大小。
FileLocktryLock()尝试获取此频道文件的排他锁。
abstract FileLocktryLock(long position, long size, boolean shared)尝试获取此通道文件的给定区域的锁定。
abstract intwrite(ByteBuffer src)从给定的缓冲区向该通道写入一个字节序列。
longwrite(ByteBuffer[] srcs)从给定的缓冲区向该通道写入一系列字节。
abstract longwrite(ByteBuffer[] srcs, int offset, int length)从给定缓冲区的子序列将一个字节序列写入该通道。
abstract intwrite(ByteBuffer src, long position)从给定的缓冲区向给定的文件位置开始,向该通道写入一个字节序列。

Buffer通常的操作

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

打开FileChannel

  • 在使用 FileChannel 之前,必须先打开它。但是,我们无法直接打开一个FileChannel,需要通过使用一个 InputStreamOutputStreamRandomAccessFile 来获取一个 FileChannel 实例
RandomAccessFile file = new RandomAccessFile(".\\src\\res\\01.txt", "rw");
//FileInputStream file = new FileInputStream(".\\src\\res\\01.txt");
//FileOutputStream file = new FileOutputStream(".\\src\\res\\01.txt");

FileChannel channel = file.getChannel();

从FileChannel读取数据

调用多个 read()方法之一,把数据从数据源,经过通道 FileChannel读取到缓冲区

ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buf);

1、分配一个 Buffer

读取的数据将被放到 Buffer 中

2、调用 FileChannel.read()方法

读取数据到 Buffer 中

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

向 FileChannel 写数据——FileChannel.write()

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

public class FileChannelDemo2 {
    public static void main(String[] args) throws Exception {
        // FileOutputStream file = new FileOutputStream("001.txt",true);
        RandomAccessFile file = new RandomAccessFile("001.txt","rw");
        FileChannel channel = file.getChannel();

        // 创建buffer,并存入数据
        ByteBuffer buf = ByteBuffer.allocate(1024);
        buf.clear();
        String str = "测试abc" + System.currentTimeMillis();
        buf.put(str.getBytes());
        // 读写转换
        buf.flip();

        // 如果buffer中有数据就写入通道
        while(buf.hasRemaining()){
            channel.write(buf);
        }

        channel.close();
    }
}

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

关闭 FileChannel——channel.close()

  • 用完 FileChannel 后必须将其关闭

    channel.close();
    

position() 方法

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

这里有两个例子:

// 获取当前位置
long pos = channel.position();
// 设置当前位置
channel.position(pos +123);

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

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

size() 方法

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

long fileSize = channel.size();

truncate() 方法

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

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

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

force() 方法

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

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

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

transferTo() 和 transferFrom() 方法

通道之间的数据传输:

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

    image-20240302150954369

  • 在 SoketChannel 的实现中,SocketChannel 只会传输此刻准备好的数据(可能不足 count 字节)。因此,SocketChannel 可能不会将请求的所有数据(count 个字节)全部传输到 FileChannel 中。

public class FileChannelDemo3 {
    public static void main(String[] args) throws Exception {
        // 创建通道1
        RandomAccessFile fromFile = new RandomAccessFile("from.txt", "rw");
        FileChannel fromChannel = fromFile.getChannel();
        // 创建通道2
        RandomAccessFile toFile = new RandomAccessFile("to.txt", "rw");
        FileChannel toChannel = toFile.getChannel();

        // 传输
        long position = 0L;
        long size = fromChannel.size();
        //toChannel.transferFrom(fromChannel, position, size);
        fromChannel.transferTo(position,size,toChannel);

        // 关闭通道
        fromChannel.close();
        toChannel.close();
        System.out.println("over!");
    }
}

3.2.2、Socket 通道

3.2.2.1 简介
Channel通道对应Socket
SocketChannelSocket
ServerSocketChannelServerSocket
DatagramChannelDatagramSocket
  • 新的 socket 通道类可以运行非阻塞模式并且是可选择的。

    再也没有为每个 socket 连接使用一个线程的必要了,也避免了管理大量线程所需的上下文交换开销。

    借助新的 NIO 类,一个或几个线程就可以管理成百上千的活动 socket 连接了,并且只有很少甚至可能没有性能损失。

    这意味着可以用一个 Selector 对象来执行 socket 通道的就绪选择(readiness selection)。

    所有的 socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)都继承了位于 java.nio.channels.spi 包中的 AbstractSelectableChannel

  • 注意 DatagramChannel 和 SocketChannel 实现定义读和写功能的接口;而ServerSocketChannel 不实现。ServerSocketChannel 负责监听传入的连接和创建新的 SocketChannel 对象,它本身从不传输数据

  • socket 和 socket 通道之间的关系

    • 通道是一个连接 I/O 服务导管并提供与该服务交互的方法
    • 就某个 socket 而言,它不会再次实现与之对应的 socket 通道类中的 socket 协议 API
    • 而 java.net 中已经存在的 socket 通道都可以被大多数协议操作重复使用
    • 全部 socket 通道类(DatagramChannel、SocketChannel 和 ServerSocketChannel)在被实例化时都会创建一个对等 socket 对象。这些是来自 java.net 的类(Socket、ServerSocket 和DatagramSocket),它们已经被更新以识别通道。对等 socket 可以通过调用 **socket( )**方法从一个通道上获取。此外,这三个 java.net 类现在都有 getChannel( ) 方法
  • 要把一个 socket 通道置于非阻塞模式,要依靠所有 socket 通道类的公有超级类:SelectableChannel

    • 就绪选择(readiness selection)是一种可以用来查询通道的机制,该查询可以判断通道是否准备好执行一个目标操作,如读或写
    • 非阻塞 I/O 和 可选择性 是紧密相连的,那也正是管理阻塞模式的 API 代码要在SelectableChannel 超级类中定义的原因
    • 设置或重新设置一个通道的阻塞模式——只要调用 configureBlocking( )方法即可,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式
    • 可以通过调用 **isBlocking( )**方法来判断某个 socket 通道当前处于哪种模式
    • 防止 socket 通道的阻塞模式被更改,API 中有一个**blockingLock( )**方法,该方法会返回一个非透明的对象引用。返回的对象是通道实现修改阻塞模式时内部使用的。只有拥有此对象的锁的线程才能更改通道的阻塞模式
3.2.2.2 ServerSocketChannel

ServerSocketChannel 是一个基于通道的 socket 监听器

  • 它同java.net.ServerSocket 执行相同的任务,不过它增加了通道语义,因此能够在非阻塞模式下运行

阻塞模式和非阻塞模式

  • configureBlocking( )方法设置阻塞切换,传递参数值为 true 则设为阻塞模式,参数值为 false 值设为非阻塞模式

  • ServerSocketChannel默认是阻塞模式;会在 SocketChannel sc = ssc.accept() 位置阻塞等待

  • ServerSocketChannel 可以设置成非阻塞模式。在非阻塞模式下,accept() 方法会立刻返回,如果还没有新进来的连接,返回的将是 null。 因此,需要检查返回的SocketChannel 是否是 null

// 设置非阻塞模式
ssc.configureBlocking(false);

// accept() 立刻返回,不会阻塞等待
SocketChannel sc = ssc.accept();
if (sc == null){
// 无连接
} else {
// 有连接
}

主要方法:

Modifier and TypeMethod and Description
abstract SocketChannelaccept()接受与此频道套接字的连接。
ServerSocketChannelbind(SocketAddress local)将通道的套接字绑定到本地地址,并配置套接字以监听连接。
abstract ServerSocketChannelbind(SocketAddress local, int backlog)将通道的套接字绑定到本地地址,并配置套接字以监听连接。
abstract SocketAddressgetLocalAddress()返回此通道的套接字所绑定的套接字地址。
static ServerSocketChannelopen()打开服务器插槽通道。
abstract ServerSocketsocket()检索与此通道关联的服务器套接字。
  • bind() 绑定端口——将通道的套接字绑定到本地地址并侦听连接。
  • accept() 监听连接——ServerSocketChannel 的 accept()方法会返回 SocketChannel 类型对象

    • 阻塞模式下运行。 其accept()方法会阻塞返回一个 Socket 对象

    • 非阻塞模式被调用,当没有传入连接在等待时,ServerSocketChannel.accept( )会立即返回 null。我们可以使用一个选择器实例来注册 ServerSocketChannel 对象以实现新连接到达时自动通知的功能。

    • // 监听
      while (true) {
          System.out.println("等待连接...");
          SocketChannel sc = ssc.accept();
          // 无连接
          if (sc == null) {
              System.out.println("null");
              Thread.sleep(2000);
          }
          // 有连接
          else {
              System.out.println("当前连接:" +
                                 sc.getRemoteAddress());
              // 重置buffer指针
              buf.rewind();
              // 把buffer数据写入通道
              sc.write(buf);
              // 关闭
              sc.close();
          }
      }
      
  • 打开 ServerSocketChannel。ServerSocketChannel类是抽象的,并不能直接new实例化,但API中提供了public static ServerSocketChannel open()方法来创建ServerSocketChannel类的实例。

  • 关闭 ServerSocketChannel
    • 通过调用 ServerSocketChannel 对象的 close() 方法来关闭 ServerSocketChannel

ServerSocketChannel使用示例:

public class ServerSocketChannelDemo {
    public static void main(String[] args) throws IOException, InterruptedException {
        // 定义端口
        int port = 9999;

        // 创建缓冲区,并存入数据
        String str = "Hello World!";
        ByteBuffer buf = ByteBuffer.wrap(str.getBytes());

        // 创建ServerSocketChannel
        ServerSocketChannel ssc = ServerSocketChannel.open();

        // 绑定端口
        ssc.socket().bind(new InetSocketAddress(port));

        // 设置非阻塞模式
        ssc.configureBlocking(false);
        System.out.println(ssc.isBlocking());
        
        // 监听是否有新的链接传入
        while (true) {
            SocketChannel accept = ssc.accept();
            if (Objects.isNull(accept)) {
                System.out.println("Waiting for connections:" + accept);
                Thread.sleep(2000);
            } else {
                // 有连接的情况
                System.out.println("当前连接:" + accept.getRemoteAddress());
                buf.rewind(); // 指针0
                accept.write(buf);
                accept.close();
            }
        }
    }
}
3.2.2.3 SocketChannel

Java NIO 中的 SocketChannel 是一个连接到 TCP 网络套接字的通道

  • SocketChannel 是用来连接 Socket 套接字
  • SocketChannel 主要用途用来处理网络 I/O 的通道
  • SocketChannel 是基于 TCP 连接传输
  • SocketChannel 实现了可选择通道,可以被多路复用

SocketChannel特征

  • 对于已经存在的 socket 不能创建 SocketChannel

  • SocketChannel 中提供的 无参open 接口创建的 Channel 并没有进行网络级联,需要使用 connect 接口连接到指定地址

  • 未进行连接的 SocketChannle 执行 I/O 操作时,会抛出NotYetConnectedException

  • SocketChannel 支持两种 I/O 模式:阻塞式非阻塞式

  • SocketChannel 支持异步关闭

    • 如果 SocketChannel 在一个线程上 read 阻塞,另一个线程对该 SocketChannel 调用 shutdownInput,则读阻塞的线程将返回-1,表示没有读取任何数据;
    • 如果 SocketChannel 在一个线程上 write 阻塞,另一个线程对该SocketChannel 调用 shutdownWrite,则写阻塞的线程将抛出AsynchronousCloseException
  • SocketChannel支持设定参数

    • SO_SNDBUF 套接字发送缓冲区大小
    • SO_RCVBUF 套接字接收缓冲区大小
    • SO_KEEPALIVE 保活连接
    • O_REUSEADDR 复用地址
    • SO_LINGER 有数据传输时延缓关闭 Channel (只有在非阻塞模式下有用)
    • TCP_NODELAY 禁用 Nagle 算法

SocketChannel方法的使用:

  • 创建 SocketChannel

    • 方式一:有参open,使用有参open(),既创建了SocketChannel对象,也进行了TCP连接

      // 创建SocketChannel对象,并进行TCP连接
      SocketChannel sc1 = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
      
    • 方式二:无参open,使用无参open(),只是创建了SocketChannel对象,需要connect()进行了TCP连接

      • connect(SocketAddress remote)连接此通道的插座。
      // 创建SocketChannel对象
      SocketChannel sc1 = SocketChannel.open();
      // 进行TCP连接
      sc1.connect(new InetSocketAddress("www.baidu.com", 80));
      
  • 连接校验

    // 是否为open状态
    sc1.isOpen();
    // 是否已经被连接
    sc1.isConnected();
    // 是否正在进行连接
    sc1.isConnectionPending();
    // 是否已经完成连接
    sc1.finishConnect();
    
  • 阻塞模式

    // 默认为阻塞模式:true
    sc1.isBlocking();
    // 设置为非阻塞模式
    sc1.configureBlocking(false);
    
  • 读写

    ByteBuffer buf = ByteBuffer.allocate(24);
    // 把源端的数据通过SocketChannel读入buf
    sc1.read(buf);
    
  • 设置和获取参数

    // 参数
    // 默认发送缓冲区大小:
    sc1.getOption(StandardSocketOptions.SO_SNDBUF);
    // 默认接收缓冲区大小:
    sc1.getOption(StandardSocketOptions.SO_RCVBUF);
    // 保活连接:
    sc1.getOption(StandardSocketOptions.SO_KEEPALIVE);
    // 禁用 Nagle 算法:
    sc1.getOption(StandardSocketOptions.TCP_NODELAY);
    // 发送缓冲区大小
    sc1.setOption(StandardSocketOptions.SO_SNDBUF, 4096);
    // 设置保活连接
    sc1.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE);
    

代码示例:

public class SocketChannelDemo {
    public static void main(String[] args) throws IOException {
        // 创建SocketChannel
        // 方式一:有参open()
        //SocketChannel sc1 = SocketChannel.open(new InetSocketAddress("www.baidu.com", 80));
        // 方式二:无参open()
        SocketChannel sc1 = SocketChannel.open();
        sc1.connect(new InetSocketAddress("www.baidu.com", 80));

        // 参数
        System.out.println("默认发送缓冲区大小:"
                + sc1.getOption(StandardSocketOptions.SO_SNDBUF));
        System.out.println("默认接收缓冲区大小:"
                + sc1.getOption(StandardSocketOptions.SO_RCVBUF));
        // 默认阻塞模式
        System.out.println("默认阻塞模式:" + sc1.isBlocking());
        System.out.println("保活连接:"
                + sc1.getOption(StandardSocketOptions.SO_KEEPALIVE));
        System.out.println("禁用 Nagle 算法:"
                + sc1.getOption(StandardSocketOptions.TCP_NODELAY));
        System.out.println("--------------");
        sc1.setOption(StandardSocketOptions.SO_SNDBUF, 4096);
        System.out.println("发送缓冲区大小:"
                + sc1.getOption(StandardSocketOptions.SO_SNDBUF));
        sc1.setOption(StandardSocketOptions.SO_KEEPALIVE, Boolean.TRUE);
        System.out.println("保活连接:"
                + sc1.getOption(StandardSocketOptions.SO_KEEPALIVE));
        System.out.println("--------------");
        System.out.println("打开:" + sc1.isOpen());
        System.out.println("连接:" + sc1.isConnected());
        System.out.println("正在连接:" + sc1.isConnectionPending());
        System.out.println("完成连接:" + sc1.finishConnect());
        System.out.println("--------------");
        // 阻塞模式
        // 设置阻塞模式
        sc1.configureBlocking(false);
        // 读取
        ByteBuffer buf = ByteBuffer.allocate(24);
        System.out.println("Reading...");
        sc1.read(buf);
        // 关闭
        sc1.close();

        System.out.println("--------------");
        System.out.println("打开:" + sc1.isOpen());
        System.out.println("连接:" + sc1.isConnected());
        System.out.println("正在连接:" + sc1.isConnectionPending());
        //System.out.println("完成连接:" + sc1.finishConnect());
        System.out.println("--------------");
        System.out.println("Read Over!");
    }
}
3.2.2.3 DatagramChannel
  • SocketChannel 模拟连接导向的流协议(如 TCP/IP),DatagramChannel 则模拟包导向的无连接协议(如 UDP/IP)
  • DatagramChannel 是无连接的,每个数据报(datagram)都是一个自包含的实体,拥有它自己的目的地址及不依赖其他数据报的数据负载
  • 与面向流的的 socket 不同,DatagramChannel 可以发送单独的数据报给不同的目的地址。同样,DatagramChannel 对象也可以接收来自任意地址的数据包。每个到达的数据报都含有关于它来自何处的信息(源地址)

DatagramChannel方法的使用:

  • 打开DatagramChannel

    // 打开DatagramChannel
    DatagramChannel sdc = DatagramChannel.open();
    
  • 发送数据——send(缓冲区,目的地址)

    // 目的地址
    InetSocketAddress ads = new InetSocketAddress("127.0.0.1", 10086);
    // 发送的数据
    String s = "测试abc" + System.currentTimeMillis();
    // 缓冲区
    ByteBuffer buf = ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8));
    // 发送
    sdc.send(buf, ads);
    
  • 接收数据,返回SocketAddress对象,包含发送方的 ip、端口等信息

    // 1、打开DatagramChannel   open()
    // 2、绑定端口 bind()
    // 3、定义接收缓冲区 buf
    // 4、接收 receive()
    SocketAddress sa = rdc.receive(buf);
    
  • 连接

    • UDP 不存在真正意义上的连接,这里的连接是向特定服务地址用 read 和 write 接收发送数据包。见示例中的connect()方法
    • read() 和 write()只有在 connect()后才能使用,不然会抛NotYetConnectedException 异常。用 read()接收时,如果没有接收到包,会抛PortUnreachableException 异常。

代码示例:

public class DatagramSocketDemo {
    /**
     * send发送
     */
    @Test
    public void send() throws IOException, InterruptedException {
        // 打开DatagramChannel
        DatagramChannel sdc = DatagramChannel.open();

        // 目的地址
        InetSocketAddress ads = new InetSocketAddress("127.0.0.1", 10086);

        // 循环发送
        while (true) {
            String s = "测试abc" + System.currentTimeMillis();
            // 缓冲区
            ByteBuffer buf = ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8));
            // 发送
            sdc.send(buf, ads);

            System.out.println(s + "发送完毕!");
            Thread.sleep(2000);
        }
    }

    /**
     * receive接收
     */
    @Test
    public void receive() throws IOException {
        // 打开DatagramChannel
        DatagramChannel rdc = DatagramChannel.open();

        // 接收端口
        InetSocketAddress ads = new InetSocketAddress(10086);

        // 绑定端口
        rdc.bind(ads);

        // 缓冲区
        ByteBuffer buf = ByteBuffer.allocate(30);

        // 循环接收
        while (true) {
            // 清空缓冲区
            buf.clear();
            // 接收
            SocketAddress sa = rdc.receive(buf);
            // 缓冲区读写反转
            buf.flip();

            System.out.println(sa.toString());
            System.out.println(Charset.forName("utf8").decode(buf));
        }
    }

    /**
     * write、read、connect
     */
    @Test
    public void connect() throws IOException {
        // 打开DatagramChannel
        DatagramChannel rdc = DatagramChannel.open();

        // 定义接收端口
        InetSocketAddress ads = new InetSocketAddress(9999);

        // 绑定端口
        rdc.bind(ads);

        rdc.connect(new InetSocketAddress("127.0.0.1", 9999));

        // 发送
        String s = "abc测试" + System.currentTimeMillis();
        // 缓冲区
        ByteBuffer buf1 = ByteBuffer.wrap(s.getBytes(StandardCharsets.UTF_8));
        // 发送
        rdc.write(buf1);
        System.out.println(s + "发送完毕!");

        // 接收
        // 缓冲区
        ByteBuffer buf = ByteBuffer.allocate(128);
        // 循环接收
        while(true){
            // 清空缓冲区
            buf.clear();
            // 读取
            rdc.read(buf);
            // 读写反转
            buf.flip();

            System.out.println(rdc.getRemoteAddress());
            System.out.println(Charset.forName("utf8").decode(buf));
        }
    }
}

3.3 分散与聚集

3.3.1 概述

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

  • 分散(scatter)。从 Channel 中读取,是指在读操作时将读取的数据写入多个 buffer中。因此,Channel 将读取的数据”分散(scatter)”到多个 Buffer中

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

scatter / gather 经常用于需要将传输的数据分开处理的场合

例如:传输一个由消息头和消息体组成的消息,可能会将消息体和消息头分散到不同的 buffer 中,这样可以方便的处理消息头和消息体

3.3.2 Scattering Reads

Scattering Reads 是指数据从一个 channel 读取到多个 buffer 中

image-20240302172119467

// 消息头缓冲区
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 才能正常工作

3.3.3 Gathering Writes

Gathering Writes 是指数据从多个 buffer 写入到同一个 channel

image-20240302172102451

// 消息头缓冲区
ByteBuffer header = ByteBuffer.allocate(128);
// 消息体缓冲区
ByteBuffer body	= ByteBuffer.allocate(1024);
// 缓冲区数组
ByteBuffer[] bufferArray = {header, body};
// 写入
channel.write(bufferArray);
  • bufferArray 数组是 write()方法的入参,write()方法会按照 buffer 在数组中的顺序,将数据写入到 channel,注意只有 position 和 limit 之间的数据才会被写入。

  • 因此,如果一个 buffer 的容量为 128byte,但是仅仅包含 58byte 的数据,那么这 58byte 的数据将被写入到 channel 中。因此与 Scattering Reads 相反,Gathering Writes 能较好的处理动态消息

四、Buffer

4.1 Buffer简介

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

数据是从通道读入缓冲区,从缓冲区写入到通道中的

image-20240302182820654

  • 缓冲区本质上是一块可以写入数据,然后可以从中读取数据的内存

    这块内存被包装成 NIO Buffer 对象,并提供了一组方法,用来方便的访问该块内存。

  • 缓冲区实际上是一个容器对象,其实就是一个数组,在 NIO 库中,所有数据都是用缓冲区处理的

    在读取数据时,它是直接读到缓冲区中的; 在写入数据时,它也是写入到缓冲区中的;任何时候访问 NIO 中的数据,都是将它放到缓冲区中。

    而在面向流 I/O 系统中,所有数据都是直接写入或者直接将数据读取到 Stream 对象中。

  • 在 NIO 中,所有的缓冲区类型都继承于抽象类 Buffer,最常用的就是 ByteBuffer,对于 Java 中的基本类型,基本都有一个具体 Buffer 类型与之相对应,它们之间的继承关系如下图所示:

image-20240302182916036

Buffer的类型:

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

这些 Buffer 类型代表了不同的数据类型。可以通过 char,short,int,long,float 或 double 类型来操作缓冲区中的字节

4.3 capacity、position和limit

  • capacity:容量

    • 作为一个内存块,Buffer 有一个固定的大小值,也叫“capacity”

    • 只能往里写capacity 个 byte、long,char 等类型

    • 一旦 Buffer 满了,需要将其清空(通过读数据或者清除数据)才能继续写数据往里写数据

  • position:指针位置

    • 向Buffer中写数据时,position 表示写入数据的当前位置,position 的初始值为0。当一个 byte、long 等数据写到 Buffer 后, position 会向下移动到下一个可插入数据的 Buffer 单元。position 最大可为 capacity – 1(因为 position 的初始值为0)

    • 从Buffer中读数据时,position 表示读入数据的当前位置,如 position=2 时表示已开始读第 3 个 byte,或从第 3 个 byte 开始读取。通过 ByteBuffer.flip()切换到读模式时 position 会被重置为 0,当 Buffer 从 position 读入数据后,position 会下移到下一个可读入的数据 Buffer 单元。

  • limit:界限

    • 写数据时,limit 表示可对 Buffer 最多写入多少个数据。写模式下,limit 等于Buffer 的 capacity。

    • 读数据时,limit 表示 Buffer 里有多少可读数据(not null 的数据),因此能读到之前写入的所有数据(limit 被设置成已写数据的数量,这个值在写模式下就是position)

属性写模式读模式
capacity容量容量
position写入数据的当前位置读取数据的当前位置
limit最多可写入的数据量有多少可读数据(not null)

image-20240302185137254

4.2 Buffer的基本用法

4.2.1 读写数据步骤

  1. 写入数据到Buffer。当向 buffer 写入数据时,buffer 会记录下写了多少数据

  2. 调用 flip() 读写反转。一旦要读取数据,需要通过flip()方法将 Buffer 从写模式切换到读模式

  3. 从Buffer中读取数据。在读模式下,可以读取之前写入到 buffer的所有数据

  4. 调用clear() 或 compact() 重置。

一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。

有两种方式能清空缓冲区:调用 clear()或 compact()方法。

  • clear()方法会清空整个缓冲区。
  • compact()方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起

始处,新写入的数据将放到缓冲区未读数据的后面。

读写示例

public class BufferDemo {
    public static void main(String[] args) throws IOException {
        // bufferTest();
        intBufferTest();
    }

    /**
     * buffer读写
     * @throws IOException
     */
    public static void bufferTest() throws IOException {
        // 目标文件路径
        String filePaht = ".\\src\\main\\resources\\03.txt";
        // 定义文件
        RandomAccessFile file = new RandomAccessFile("file/03.txt", "rw");
        // 获取通道
        FileChannel channel = file.getChannel();

        // 定义接收缓冲区,先从通道写到缓冲区
        ByteBuffer buf = ByteBuffer.allocate(32);

        // 单次读取数量
        int readNum = 0;
        // 如果没有读到文件结尾就循环读取;把读到的数据写入buf
        while((readNum = channel.read(buf)) != -1){
            // 读取反转:由写入转为读取
            buf.flip();
            // 如果buf中有数据
            if (buf.hasRemaining()){
                // 输出数据:utf8编码
                System.out.print(Charset.forName("utf8").decode(buf));
            }
            // 重置buf
            buf.clear();
            // buf.compact();
        }
        // 在关闭通道前,调用file,输出正常
        System.out.println(file.length());
        // file.close();
        // 关闭通道:通道关闭后,file也会关闭
        channel.close();

        // 关闭通道后,再调用file,会出异常;因为file也关闭了
        // System.out.println(file.length());
    }
}

image-20240302184610406

IntBuffer示例

    /**
     * IntBuffer读写
     */
    public static void intBufferTest() {
        // 定义buffer
        IntBuffer buf = IntBuffer.allocate(8);
        // 写入buffer
        for (int i = 0; i < buf.capacity(); i++) {
            buf.put(2 * (i + 1));
        }
        // 读写反转
        buf.flip();
        // 读取
        while (buf.hasRemaining()) {
            System.out.print(buf.get() + " ");
        }
    }

在这里插入图片描述

4.2.2 Buffer分配和写数据

  • 分配 allocate()。要想获得一个 Buffer 对象首先要进行分配。 每一个 Buffer 类都有一个 allocate 方法

    // 分配一个可存储48字节的Buffer
    ByteBuffer buf1 = ByteBuffer.allocate(48);
    
    
    // 分配一个可存储1024字符的Buffer
    CharBuffer buf2 = ByteBuffer.allocate(1024);
    
  • 写数据 read()、put()

    • read():从通道Channel读取,并写入Buffer

      int bytesRead = inChannel.read(buf);
      
    • put():用Buffer的put()方法写入

      buf.put(127);
      

      put 方法有很多版本,允许你以不同的方式把数据写入到 Buffer 中。例如, 写到一个指定的位置,或者把一个字节数组写入到 Buffer

  • flip()

    • flip 方法将 Buffer 从写模式切换到读模式

    • 调用 flip()方法会将 position 设回 0,并将 limit 设置成之前 position 的值。position 现在用于标记读的位置,limit 表示之前写进了多少个 byte、char 等 (现在能读取多少个 byte、char 等)。

4.2.3 从Buffer中读取数据

  • write():从Buffer中读取数据,并写入Channel

    int bn = inChannel.write(buf);
    
  • get():用Buffer的方法读取

    byte b = buf.get();
    

    get 方法有很多版本,允许以不同的方式从 Buffer 中读取数据。例如,从指定position 读取,或者从 Buffer 中读取数据到字节数组。

4.2.4 Buffer其他常用方法

  • **rewind()。**将 position 设回 0,limit 保持不变。可以重读 Buffer 中的所有数据。limit 保持不变,仍然表示能从 Buffer 中读取多少个元素(byte、char 等)

  • clear() 与 compact()

    • clear()。position 将被设回 0,limit 被设置成 capacity 的值。相当于Buffer 被清空了。Buffer 中的数据并未清除,只是这些标记告诉我们可以从哪里开始往 Buffer 里写数据。

    • compact()。将所有未读的数据拷贝到 Buffer 起始处。然后将 position 设到最后一个未读元素后面。limit 属性依然像 clear()方法一样,设置成 capacity。现在Buffer 准备好写数据了,但是不会覆盖未读的数据。

区别positionlimit
rewind()0保持不变
clear()0= capacity
compact()最后一个未读元素后面= capacity
  • mark() 与 reset()

    • mark()。标记 Buffer 中的一个特定 position

    • reset()。恢复到之前标记的 position

Buffer方法汇总:

方法介绍
abstract Object array()返回支持此缓冲区的数组 (可选操作)
abstract int arrayOffset()返回该缓冲区的缓冲区的第一个元素的背衬数组中的偏移量 (可选操作)
int capacity()返回此缓冲区的容量
Buffer clear()清除此缓存区。将position = 0;limit = capacity;mark = -1;
Buffer flip()flip()方法可以吧Buffer从写模式切换到读模式。调用flip方法会把position归零,并设置limit为之前的position的值。 也就是说,现在position代表的是读取位置,limit标示的是已写入的数据位置。
abstract boolean hasArray()告诉这个缓冲区是否由可访问的数组支持
boolean hasRemaining()return position < limit,返回是否还有未读内容
abstract boolean isDirect()判断个缓冲区是否为 direct
abstract boolean isReadOnly()判断告知这个缓冲区是否是只读的
int limit()返回此缓冲区的限制
Buffer position(int newPosition)设置这个缓冲区的位置
int remaining()return limit - position; 返回limit和position之间相对位置差
Buffer rewind()把position设为0,mark设为-1,不改变limit的值
Buffer mark()将此缓冲区的标记设置在其位置

4.4 缓冲区操作

4.4.1 缓冲区分片 slice()

  • 在 NIO 中,除了可以分配或者包装一个缓冲区对象外,还可以根据现有的缓冲区对象来创建一个子缓冲区,即在现有缓冲区上切出一片来作为一个新的缓冲区,但现有的缓冲区与创建的子缓冲区在底层数组层面上是数据共享的,也就是说,子缓冲区相当于是现有缓冲区的一个视图窗口。

  • 调用 **slice()**方法可以创建一个子缓冲区。

@Test
public void slice() {
  // 创建Buffer
  IntBuffer buf = IntBuffer.allocate(10);
  // 写入
  for (int i = 0; i < buf.capacity(); i++) {
    buf.put(i);
  }

  // 创建子缓冲区
  buf.position(3);
  buf.limit(7);
  IntBuffer sliceBuf = buf.slice();
  // 子缓冲区写入
  for (int i = 0; i < sliceBuf.capacity(); i++) {
    int s = sliceBuf.get(i);
    s *= 10;
    sliceBuf.put(s);
  }
  // 输出
  buf.position(0);
  buf.limit(buf.capacity());
  while(buf.remaining() > 0){
    System.out.print(buf.get() + " ");
  }
}

image-20240103163425803

4.4.2 只读缓冲区 asReadOnlyBuffer()

  • 只读缓冲区非常简单,可以读取它们,但是不能向它们写入数据。可以通过调用缓冲区的 **asReadOnlyBuffer()**方法,将任何常规缓冲区转换为只读缓冲区,这个方法返回一个与原缓冲区完全相同的缓冲区,并与原缓冲区共享数据,只不过它是只读的。
  • 如果原缓冲区的内容发生了变化,只读缓冲区的内容也随之发生变化
@Test
public  void readOnlyBuffer() {
  // 创建Buffer
  IntBuffer buf = IntBuffer.allocate(10);
  // 写入
  for (int i = 0; i < buf.capacity(); i++) {
    buf.put(i);
  }

  // 得到只读缓冲区
  IntBuffer readOnlyBuffer = buf.asReadOnlyBuffer();
  // 不能向只读缓冲区写数据,会抛异常
  //        for (int i = 0; i < readOnlyBuffer.capacity(); i++) {
  //            readOnlyBuffer.put(i*10);
  //        }

  // 写入新数据
  for (int i = 0; i < buf.capacity(); i++) {
    buf.put(i,i * 10);
  }

  // 重新新位置
  readOnlyBuffer.position(0);
  readOnlyBuffer.limit(readOnlyBuffer.capacity());
  System.out.println("只读buf位置:"+readOnlyBuffer.position() + " " + readOnlyBuffer.limit());
  // 输出
  while(readOnlyBuffer.hasRemaining()){
    System.out.print(readOnlyBuffer.get() + " ");
  }
}

image-20240103170726598

  • 如果尝试修改只读缓冲区的内容,则会报 ReadOnlyBufferException 异常。
  • 只读缓冲区对于保护数据很有用。在将缓冲区传递给某个对象的方法时,无法知道这个方法是否会修改缓冲区中的数据。创建一个只读的缓冲区可以保证该缓冲区不会被修改。
  • 只可以把常规缓冲区转换为只读缓冲区,而不能将只读的缓冲区转换为可写的缓冲区。

4.4.3 直接缓冲区 allocateDirect()

  • 直接缓冲区是为加快 I/O 速度,使用一种特殊方式为其分配内存的缓冲区,JDK 文档中的描述为:给定一个直接字节缓冲区,Java 虚拟机将尽最大努力直接对它执行本机I/O 操作。也就是说,它会在每一次调用底层操作系统的本机 I/O 操作之前(或之后),尝试避免将缓冲区的内容拷贝到一个中间缓冲区中或者从一个中间缓冲区中拷贝数据。

  • 要分配直接缓冲区,需要调用 **allocateDirect()**方法,而不是 allocate()方法,使用方式与普通缓冲区并无区别

public void bufferDirect() throws IOException {
  // 输入通道
  String inFilePath = "file/04.txt";
  FileInputStream fis = new FileInputStream(inFilePath);
  FileChannel fiChannel = fis.getChannel();

  // 输出通道
  String outFilePath = "file/04_.txt";
  FileOutputStream fos = new FileOutputStream(outFilePath);
  FileChannel foChannel = fos.getChannel();

  // 创建直接缓冲区
  ByteBuffer buf = ByteBuffer.allocateDirect(64);

  // 复制
  long a = fiChannel.size();
  int b = buf.capacity();
  System.out.println("文件大小:" + a + "字节");
  System.out.println("缓冲区大小:" + b + "字节");
  System.out.println("共需要复制:" + (int)Math.ceil(a/(float)b));
  int i = 0;
  // 如果还有数据可以写入buf,就循环操作
  while (fiChannel.read(buf) != -1) {
    i++;
    // 读写反转
    buf.flip();
    // 从buf中读取数据,写入通道,并存入文件
    foChannel.write(buf);
    // 清空buf
    buf.clear();
  }
  System.out.println("实际复制次数:" + i);
  System.out.println("复制完成");

  // 关闭
  fiChannel.close();
  foChannel.close();
}

4.4.4 内存映射文件I/O

  • 内存映射文件 I/O 是一种读和写文件数据的方法,它可以比常规的基于流或者基于通道的 I/O 快的多

  • 内存映射文件 I/O 是通过使文件中的数据出现为内存数组的内容来完成的,这其初听起来似乎不过就是将整个文件读到内存中,但是事实上并不是这样。一般来说,只有文件中实际读取或者写入的部分才会映射到内存中

    @Test
    public void mapIO() throws IOException {
        int start = 0;
        int size = 1024;

        RandomAccessFile raf = new RandomAccessFile("file/04.txt", "rw");
        FileChannel fc = raf.getChannel();
        MappedByteBuffer mbb = fc.map(FileChannel.MapMode.READ_WRITE, start, size);

        mbb.put(0, (byte) 97);
        mbb.put(1023, (byte) 122);

        fc.close();
    }
  • 15
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值