IO

BIO

通常在进行同步IO操作时,如果读取数据,代码会阻塞直至有可供读取的数据。同样,写入调用将会阻塞直至数据能够写入。传统的Server/Client模式会基于TPR(Thread per Request),服务器会为每个客户端请求建立一个线程,由该线程单独负责处理一个客户端请求。这种模式带来的一个问题就是线程数量剧增,大量的线程会增大服务器开销。大多数的实现为了避免这个问题,都采用了线程池,并设置线程池的最大数量。但是 这带来了一个新的问题,如果线程池中有100个线程,而且有100个用户都在进行大文件下载,会导致第101个用户的请求无法及时处理,即便是下载几KB的文件。
同步阻塞IO(传统阻塞型IO),服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程来进行处理,如果这个连接不做任何事情就会造成不必要的线程开销。
image.png

NIO

NIO中非阻塞IO采用了基于Reactor模式的工作方式,IO调用不会被阻塞,相反是注册感兴趣的特定IO事件,如可读数据到达。新的套接字连接等等,在发生特定事件时,系统再通知我们。NIO中实现非阻塞IO的核心对象是Selector,Selector就是注册各种IO事件的地方,而且当我们感兴趣的事件发生时,就是这个对象告诉我们所发生的事件。
同步非阻塞IO,服务器实现模式为一个线程可以处理多个连接请求,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有IO请求就进行处理。
image.png
从上图中可以看出,当有读或者写等任何注册的事件发生时,可以从Selector中获取到相应的SelectionKey,同事同SelectionKey中可以找到发生的事件和该事件所发生的具体的SelectableChannel,以获取到客户端发过来的数据。
非阻塞值的是IO事件本身不阻塞,但是获取IO事件的select()方法是需要阻塞等待的。
区别是阻塞的IO会阻塞在IO操作上,NIO阻塞在事件的获取上,没有事件就没有IO,从高层次看IO就不阻塞了。也就是说只有IO发生我们才评估IO是否阻塞,但是select()阻塞的时候IO还没有发生,谈何IO阻塞呢?NIO的本质是延迟IO操作到真正发生IO的时候,而不是以前的只要IO流打开了就一直等待IO操作。
image.png
image.png

Channel简述

channel是一个通道,可以通过他读取和写入数据,它就像水管一样,网络数据通过Channel读取和写入,通道和流的不同之处在于通道是双向的,流只是在一个方向上移动(一个流必须是InputStream或者OutoutStream的子类),而且通道可以用于读、写或者是同时用于读写。因为通道是全双工的,所以他可以比流更好的映射底层操作系统的API。
NIO中通过channel封装了对数据源的操作,通过channel我们可以操作数据源,但是又不必关心数据源的具体物理结构。这个数据源可能是多重的。比如,可以是文件,也可以是网络socket。在大多数应用中,channel与文件描述符或者socket是一一对应的,Channel用于在字节缓冲区和位于通道另一侧的实体(通常是一个文件或套接字)之间有效地传输数据。
image.png
与缓冲区不同,通道API主要由接口指定。不同的操作系统上通道实现会有根本性的差异,所以通道API仅仅描述了可以做什么。因此很自然的,通道实现经常使用操作系统的本地代码。通道接口允许您以一种受控且可移植的方式来访问底层的I/O服务。
Channel是一个对象,可以通过它读取和写入数据。拿NIO与原来的I/O做个对比。通道就像是流。所有数据都通过Buffer对象来处理。您永远不会将字节直接写入通道中,相反,您是将数据写入包含一个或者多个字节的缓冲区。同样的您不会直接从通道中读取字节,而是将数据从通道读入到缓冲区,然后再从缓冲区获取字节。
Java NIO的通道类似于流,但是又有些不同:

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

image.png

Channel的实现

NIO中最重要的Channel的实现:

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

如上可以看出,这些通道涵盖了UDP、TCP网络IO以及文件IO

FileChannel的介绍和示例

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

方法 描述
int read(ByteBuffer dst) 从channel中读取数据到ByteBuffer中
long read(ByteBuffer[] dsts) 从Channel中的数据“分散到”ByteBuffer[]
int write(ByteBuffer src) 将ByteBuffer中的数据写入到Channel
long write(ByteBuffer[] srcs) 将ByteBuffer[]中的数据“聚集”到Channel
long position() 返回此通道的文件位置
FileChannel position(long newPosition) 设置此通道的文件位置
long size() 返回此通道文件的当前大小
FileChannel truncate(long size) 将此通道文件截取为指定大小
void force(boolean metaData) 强制将所有对此通道文件更新写入到存储设备中

Buffer通常的操作

  • 将数据写入到缓冲区
  • 调用buffer.flip()反转读写模式
  • 从缓冲区中读取数据
  • 调用buffer.clear()或buffer.compact()清除缓冲区内容
public class FileChannelReadTest {
    public static void main(String[] args) throws Exception {
        // 创建一个FileChannel
        RandomAccessFile accessFile
                = new RandomAccessFile("/Users/ClearRain/Downloads/file_createTest_01.txt","rw");
        FileChannel channel = accessFile.getChannel();
        // 创建Buffer
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 读取数据到buffer中
        // bytesRead = -1 表示文件读取结束
        int bytesRead = channel.read(buffer);

        while (bytesRead != -1) {
            System.out.println("读取了 : " + bytesRead);
            buffer.flip();
            while (buffer.hasRemaining()) {
                System.out.println((char) buffer.get());
            }
            buffer.clear();
            bytesRead = channel.read(buffer);
        }
        accessFile.close();
        System.out.println("=== 操作结束 ===");

    }
}

FileChannel操作详解

1、打开FileChannel

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

    // 创建一个FileChannel
    RandomAccessFile accessFile
        = new RandomAccessFile("/Users/ClearRain/Downloads/file_createTest_01.txt","rw");
    FileChannel channel = accessFile.getChannel();

2、从FileChannel中读取数据

先将管道的数据读到buffer中,然后再从Buffer中读取数据
调用多个read()方法之一从FileChannel中读取数据。如:

ByteBuffer buffer = ByteBuffer.allocate(1024);
// 读取数据到buffer中
// bytesRead = -1 表示文件读取结束
int bytesRead = channel.read(buffer);

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

3、向FileChannel写数据

先将内容写入到Buffer,然后再从Bufer中写入到管道

public class FileChannelWriteTest {
    public static void main(String[] args) throws Exception {
        // 获取文件流
        RandomAccessFile accessFile
                = new RandomAccessFile("/Users/ClearRain/Downloads/file_write_test.txt","rw");
        // 打开FileChannel
        FileChannel channel = accessFile.getChannel();

        //创建Buffer对象
        ByteBuffer buffer = ByteBuffer.allocate(1024);

        String newData = "write file test h哈哈哈";
        buffer.clear();

        buffer.put(newData.getBytes());
        buffer.flip();

        // filechannel完成写
        while (buffer.hasRemaining()) {
            channel.write(buffer);
        }
        // 关闭Channel
        channel.close();
    }
}

注意FileChannel.write()实在wile循环中调用的。因为无法保证write()方法能一次性向FileChannel写多多少字节,因此需要重复调用write()方法,知道Buffer中没有尚未写入通道中的数据字节。

4、关闭FileChannel

用完FileChannel后必须将其关闭。

// 关闭Channel
channel.close();

5、FileChannel中的position方法

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

FileChannel channel = accessFile.getChannel();
long position = channel.position();

以下就是在获取到Channel后通过position(1)方法设置了当前管道从第一个字节后开始读取内容。

FileChannel channel = accessFile.getChannel();
channel = channel.position(1);

注意以下会产生一个问题:
position相当于获取到channel全部的字节大小,如果在文件结束符之后向通道中写数据,文件将撑大到设置的位置并写入数据。这将会导致“文件空洞”,磁盘上物理文件中写入的数据间有空隙。

FileChannel channel = accessFile.getChannel();
long position = channel.position();
channel = channel.position(position + 123);

6、FileChannel的size方法

返回该FileChannel示例所关联的文件大小。如:

FileChannel channel = accessFile.getChannel();
long size = channel.size();

7、FileChannel的truncate方法

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

FileChannel channel = accessFile.getChannel();
channel.truncate(10);

这个例子将会截取文件前10个字节。

8、FileChannel的force方法

FileChannel.force()方法将通道里尚未写入磁盘的数据强制写入到磁盘上。出于性能方面的考虑,操作系统会将数据缓存在内存中,所以无法保证写入到FileChannel里的数据一定会即时写到磁盘上,为了保证这一点,需要使用该方法。
force()方法有一个boolean参数,指明是否同时将文件元数据(权限信息等)写到磁盘上。

9、FileChannel的transferTo和transferFrom方法

通道之间的数据传输
如果两个通道中有一个是FileChannel,那么我们可以将数据从一个Channel传输到另外一个Channel。

  • transferFrom方法

参数中的channel传给调用channel
可以将数据从源通道传输到FileChannel中。

public class FileChannelTransferFromTest {
    public static void main(String[] args) throws Exception {
        RandomAccessFile fromAccessFile
                = new RandomAccessFile("/Users/ClearRain/Downloads/transferFrom_01.txt","rw");
        FileChannel fromChannel = fromAccessFile.getChannel();

        RandomAccessFile toAccessFile
                = new RandomAccessFile("/Users/ClearRain/Downloads/transferTo_01.txt","rw");
        FileChannel toChannel = toAccessFile.getChannel();

        long position = 0;
        long count = fromChannel.size();
    	// 将fromChannel的数据传输到toChannel
        toChannel.transferFrom(fromChannel,position,count);

        fromAccessFile.close();
        toAccessFile.close();
        System.out.println("over!!!");
    }
}

image.png

  • transferTo() 方法

调用channel传给参数channel
transferTo()方法将数据从FileChannel中传输到其他channel中

public class FileChannelTransferFromTest {
    public static void main(String[] args) throws Exception {
        RandomAccessFile fromAccessFile
                = new RandomAccessFile("/Users/ClearRain/Downloads/transferFrom_01.txt","rw");
        FileChannel fromChannel = fromAccessFile.getChannel();

        RandomAccessFile toAccessFile
                = new RandomAccessFile("/Users/ClearRain/Downloads/transferTo_01.txt","rw");
        FileChannel toChannel = toAccessFile.getChannel();

        long position = 0;
        long count = fromChannel.size();
    	// 将fromChannel数据传输到toChannel
        fromChannel.transferTo(position,count,toChannel);

        fromAccessFile.close();
        toAccessFile.close();
        System.out.println("over!!!");
    }
}

AIO

异步非阻塞IO

posted @ 2022-11-16 22:57  ClearRain  阅读(133)  评论(0)    收藏  举报