linux IO

IO

io定义

IO (Input/Output,输入/输出,其定义是站在CPU和内存的角度进行定义、研究),即数据的读入或写出操作,通常用户进程中的一个完整IO分为两阶段:用户进程空间至内核空间、内核空间至设备空间(磁盘、网络等)。IO主要分为网络IO和磁盘IO两种。理解系统IO是理解java 相关io模型的基础。

磁盘io

磁盘I/O的访问方式又包括以下几种:缓存I/O、直接I/O以及内存映射

缓存 i/o

在这里插入图片描述

上图,操作系统将内存分为两部分,分别为内核部分与用户部分,其容量大小比值为1:3,内核由操作系统直接控制使用,用户部分由操作系统分配给应用程序使用,我们应用程序通常使用user这一部分。kennel buffer是内核中分配给磁盘做缓存的内存块,user buffer是用户空间读入文件存储的内存块。

  1. 读入:当应用程序需要从磁盘中读取一个文件的时候,应用程序调用操作系统提供的接口,操作系统收到指令以后,切换到内核进程,检查kennel buffer里面是否有所需要的数据,如果有,则直接返回;如果没有,则调用磁盘驱动程序读取所需要文件,缓存至kennel buffer,然后再返回应用线程,用户线程决定对所取的数据进行处理。通常对于应用线程来讲,这一操作是同步阻塞的
  2. 写出:当应用程序需要将数据写出到磁盘的时候,应用线程将指令传递给操作系统,操作系统将数据写入到缓存页中(图中kennel),然后返回给应用线程。操作系统会根据一定策略,延迟将数据刷新到磁盘中。
直接IO

在这里插入图片描述

一般来说,上面介绍的缓冲IO已经足够应付日常需求了。但是像数据库这种极度依赖IO的应用程序,为了追求极致的性能,往往更加愿意自己直接操作磁盘。

直接IO可以直接将数据从磁盘复制到用户空间,或者将数据从用户空间写到磁盘,减少了kernel中的缓冲区这一环节,这是直接IO可以提高性能的原理。

对于java语言来讲,没有可直接操作直接IO的API接口,在 Java 中使用 Direct IO 最终需要调用到 c 语言的 pwrite 接口,并设置 O_DIRECT flag,使用 O_DIRECT 存在不少限制:

  • 操作系统限制:Linux 操作系统在 2.4.10 及以后的版本中支持 O_DIRECT flag,老版本会忽略该 Flag;Mac OS也有类似于 O_DIRECT 的机制

  • 用于传递数据的缓冲区,其内存边界必须对齐为 blockSize 的整数倍

  • 用于传递数据的缓冲区,其传递数据的大小必须是 blockSize 的整数倍。

  • 数据传输的开始点,即文件和设备的偏移量,必须是 blockSize 的整数倍。

查看系统 blockSize 大小的方式:stat /boot/|grep “IO Block”
ubuntu@VM-30-130-ubuntu:~$ stat /boot/|grep “IO Block” Size: 4096 > Blocks: 8 IO Block: 4096 directory
通常为 4kb

引入依赖:

<dependency>
    <groupId>moe.cnkirito.kdio</groupId>
    <artifactId>kdio-core</artifactId>
    <version>1.0.0</version>
</dependency> 
内存映射

在这里插入图片描述

mmap方法会返回一个void *类型的指针ptr,它指向进程逻辑空间中的一个地址。后续如果想要读写文件,无需调用read/write方法, 而是直接操作这个ptr指针即可。

用户试图向ptr指针指向的内存空间读写数据时,由于MMU无法在物理内存中找到对应的地址,会触发一次缺页中断,OS会去硬盘中找到对应的数据并复制到内存中,然后用户就能正常完成读写操作了。这个过程是由操作系统自动完成的。

为什么说内存映射效率比缓冲IO要高?

我们回忆一下缓存IO的工作流程:

  1. 用户调用read方法

  2. 系统调用,触发中断,进程从用户态进入内核态

  3. 从硬盘中读取数据并复制到kernel缓冲区

  4. 将数据从kernel缓存区复制到用户提供的byte数组中

  5. 进程从内核态返回到用户态

完成

从上面的流程中我们可以看到,调用一次read方法,最多可能会引起两次 用户态与内核态之间的切换,以及两次数据复制

而内存映射呢?

  1. 用户试图访问ptr指向的数据

  2. MMU解析失败,触发缺页中断,程序从用户态进入到内核态

  3. 从硬盘中读取数据并复制到进程空间中ptr指向的逻辑空间里

  4. 进程从内核态返回到用户态

完成

可以看出,试图访问内存映射文件,最多可能会引起 两次用户态与内核态之间的切换,以及一次数据复制

也就是说,内存映射与缓冲IO相比,可以节省数据复制带来的开销,因此效率较高。

java实现代码示例如下:

也可参考java mmap示例:

   @SneakyThrows
    @Test
    public void testMmap(){
        String text =new StringBuilder("大王叫我去巡山\n").append("巡到一处花果山\n").toString();
        byte[] bytes = text.getBytes(StandardCharsets.UTF_8);
        //创建映射
        RandomAccessFile randomAccessFile = new RandomAccessFile(new File("d:\\巡山.txt"), "rw");
        MappedByteBuffer mappedByteBuffer =  randomAccessFile.getChannel().map(FileChannel.MapMode.READ_WRITE,0,bytes.length);
        //数据写入文件
        mappedByteBuffer.put(bytes);
        mappedByteBuffer.clear();
        //取消映射
        Cleaner cleaner = ((DirectBuffer)mappedByteBuffer).cleaner();
        if(cleaner!=null){
            cleaner.clean();
        }
    }

网络IO

普通网络IO

对于读取网络的数据,当从网卡的数据到达socket buffer以后,应用会将其数据读入到用户内存里面,然后根据程序的指标,可以将数据存储到磁盘,也可计算以后,返回给 socket buffer,并发送至远程计算机。
如下图所示:

写至网络数据 应用系统可将内存里面的数据写至 socket buffer,然后操作系统将数据发送至网卡,送至网络上的远程计算机。
如下图所至:
在这里插入图片描述

sendfile函数

sendfile函数是linux内核自带的系统函数,专门用于处理从磁盘中加载数据,并传送至网络的场景。其数据传输模式如下图所示:

在这里插入图片描述

对比
在这里插入图片描述

相比普通的网络IO模式,其减少了硬盘缓存到用户内存,用户内存到网络缓存的数据复制,且减少了kennel到user和user到kennel的cpu上下文切换。
总结:相比普通缓存类的IO模式,其减少了二次数据复制和两次的CPU上下切换。

Java IO

java的io是基于操作系统进行实现的,可分BIO,NIO,AIO

BIO

即block-io,对于用户线程来讲,其属于同步阻塞的IO。即线程发出读取或者写指令后,需要等待动作完成,收到结果才可能进行一步指令。形如传统的文件操作代码:

@Slf4j
public class JavaIOTest {

    File file = new File("d:\\a.txt");

    @SneakyThrows
    @Test
    public void bioWriteTest(){

        OutputStream out = new FileOutputStream(file);
        String txt = "好好学习,天天向上";
        out.write(txt.getBytes(StandardCharsets.UTF_8));
        out.close();
    }

    @SneakyThrows
    @Test
    public void bioReadTest(){
        InputStream inputStream = new FileInputStream(file);
        byte[] buffer  = new byte[1024];
        inputStream.read(buffer);
        inputStream.close();
        log.info("文件内容为:{}",new String(buffer,StandardCharsets.UTF_8));
    }
}

其通常使用缓存IO模型。

磁盘 内核内存 用户内存 磁盘IO 内存IO 内存IO 磁盘IO 磁盘 内核内存 用户内存

对于网络IO的情况,简单的阻塞示例演示(供应演示数据传输,不具备生产实用价值)。

@Slf4j
public class BioServer {

    @SneakyThrows
    public void start() {
        ServerSocket serverSocket = new ServerSocket(3301);
        while (true) {
            Socket clientSocket = serverSocket.accept();
            new Thread(() -> {
                try {

                    byte[] datas = new byte[1024];
                    clientSocket.getInputStream().read(datas);
                    String receive = new String(datas, StandardCharsets.UTF_8);
                    log.info("服务端收到的数据为:{}", receive);

                    log.info("数据-->客户端");
                    clientSocket.getOutputStream().write("收到了,over\n".getBytes(StandardCharsets.UTF_8));
                    clientSocket.getOutputStream().flush();
                } catch (IOException e) {
                    log.info("", e);
                }


            }).start();
        }
    }

    public static void main(String[] args) {
        new BioServer().start();
    }

    @SneakyThrows
    @Test
    public void testClient() {

        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 3301), 2000);
        log.info("连接上服务器了,开始发送数据");
       //发送数据给服务端
        PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
        printWriter.println("大王叫我来巡山");
        printWriter.println("巡到一个花果山");
        printWriter.println("是不是想当王,是不是想登基");
        printWriter.flush();

        //接收服务端数据
        byte[] datas = new byte[256];
        socket.getInputStream().read(datas);
        log.info("客户端收到的数据={}",new String(datas,StandardCharsets.UTF_8));
        socket.close();
    }
}

网络BIO,典型特性是,是有一个线程负责接收连接请求,并将建立好的连接委托给另外的业务线程执行。

  • 业务线程需要等待数据写入到socket buffer,然后再从socket buffer里面复制到应用内存,才可以进行处理。
  • 当业务处理完成以后,业务线程需要将数据回写到socket buffer中,再由操作系统发送到网络中去。

此两个过程,业务线程都会由于IO的缓慢而发生阻塞,特别是读取阶段,线程需要等待数据从网络IO上面传到socket buffer,然后才开始复制到内存。发送数据要好一些,只需要将数据发送到socket buffer,业务线程任务就完成了,后续从socket buffer 到网络,由操作系统控制处理。如下图:

在这里插入图片描述

NIO

java的nio 原意为 new io 。从JDK1.4开始,Java提供了一系列改进的输入/输出处理的新特性,被统称为NIO(即New I/O)。新增了许多用于处理输入输出的类,这些类都被放在java.nio包及子包下,并且对原java.io包中的很多类进行改写,新增了满足NIO的功能。 这里 new io 跟非阻塞io没有强相关。
java nio 有三个核心概念:

  • Channel :通道,是指跟某块IO设备连接的通道。
  • Buffers:内存块,处于用户内存。
  • Selectors: 选择器。
channel

channel与流的几个重点区别

  • 通道可以读也可以写,流一般来说是单向的(只能读或者写,所以之前我们用流进行IO操作的时候需要分别创建一个输入流和一个输出流)。

  • 通道可以异步读写。

  • 通道总是基于缓冲区Buffer来读写。

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

  • FileChannel: 用于文件的数据读写

  • DatagramChannel: 用于UDP的数据读写

  • SocketChannel: 用于TCP的数据读写,一般是客户端实现

  • ServerSocketChannel: 允许我们监听TCP链接请求,每个请求会创建会一个SocketChannel,一般是服务器实现
    下图是一个简单的 channel示例图:
    在这里插入图片描述
    其中 FileChannel和 SocketChannel均为双向通道。

Buffer

Buffer为用户内存中的一块区域,channel可以将数据写入其中,也可以将其中的数据读出,发送到外部设备。
典型的实现有ByteBuffer,CharBuffer,ShortBuffer,IntBuffer,FloatBuffer,DoubleBuffer,LongBuffer等。

利用Buffer读写数据,通常遵循四个步骤:

  1. 把数据写入buffer;
  2. 调用flip;
  3. 从Buffer中读取数据;
  4. 调用buffer.clear()或者buffer.compact()。
Selector

在这里插入图片描述
selector是一个轮询器,他会实时轮询注册到他上面的所有通道的状态。

//创建selector
Selector selector = Selector.open();
//设置通道为非阻塞。
channel.configureBlocking(false);
//注册通道到轮询器上面。
SelectionKey key = channel.register(selector, Selectionkey.OP_READ);

Channel必须是非阻塞的。
所以FileChannel不适用Selector,因为FileChannel不能切换为非阻塞模式,更准确的来说是因为FileChannel没有继承SelectableChannel。Socket channel可以正常使用。

Selector的详细使用介绍,请参考 Java NIO之Selector(选择器)

接下来,我们看一下 在java nio 中,对文件的基本操作,通过fileChannel,
注意:此过程对用户线程来讲,还是阻塞的,并没有本质改变。
代码示例:
FileChannel:

 @Slf4j
public class FileChannelTest {

    @SneakyThrows
    @Test
    public void testWriteFileChannel() {

        String str = RandomStringUtils.randomPrint(20,40);
        //写
        FileChannel fileChannel = FileChannel.open(Paths.get("d:\\a.txt"), StandardOpenOption.WRITE, StandardOpenOption.READ);
        ByteBuffer byteBuffer = ByteBuffer.allocate(256);
        log.info("str={}",str);
        byteBuffer.put(str.getBytes(Charset.defaultCharset()));
        byteBuffer.flip();
        while (byteBuffer.hasRemaining()){
            fileChannel.write(byteBuffer);
        }
        byteBuffer.clear();
    }


    @SneakyThrows
    @Test
    public void testReadFileChannel(){

        //读
        ByteBuffer byteBuffer = ByteBuffer.allocate(256);
        FileChannel fileRead = FileChannel.open(Paths.get("d:\\a.txt"),  StandardOpenOption.READ);
        fileRead.read(byteBuffer);
        byteBuffer.flip();
        String result = new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit(), StandardCharsets.UTF_8);
        log.info("读入的数据为:{}", result);

    }

    @SneakyThrows
    @Test
    public void testCopyFileChannel(){

        //复制
        ByteBuffer byteBuffer = ByteBuffer.allocate(256);
        FileChannel fileRead = FileChannel.open(Paths.get("d:\\a.txt"),  StandardOpenOption.READ);
        fileRead.read(byteBuffer);
        byteBuffer.flip();
        FileChannel writeChannel = FileChannel.open(Paths.get("d:\\b.txt"), StandardOpenOption.WRITE,StandardOpenOption.CREATE);
        while (byteBuffer.hasRemaining()){
            writeChannel.write(byteBuffer);
        }
        byteBuffer.clear();

        // fileRead.transferTo(0,fileRead.size(),writeChannel);,复制数据,也可这样。
    }
}

SocketChannel
我们接下来看一下,在网络io中的 nio Server

@Slf4j
public class NioServer {

    static int selectCount = 1;
    static int threadNameIndex = 0;

    public static void main(String[] args) {
        new NioServer().start();
    }

    @SneakyThrows
    public void start() {
        //生成一个轮询器
        Selector selector = Selector.open();
        //生成连接通道。
        ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
        //绑定主机、端口
        serverSocketChannel.bind(new InetSocketAddress("localhost", 3301));
        //设置连接通道非阻塞。
        serverSocketChannel.configureBlocking(false);
        //注册事件
        serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {       
            if (selector.select(10) < 1) {
                continue;
            }

            log.info("第{}次轮询", selectCount);
            Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
            int keyCount = 1;
            while (iterator.hasNext()) {
                log.info("第{}次轮询的第{}次轮key,Key数量={}", selectCount, keyCount++, selector.selectedKeys().size());
                SelectionKey key = iterator.next();

                //1.连接请求的数据来到
                if (key.isAcceptable()) {
                    log.info("处理连接事件");
                    handleAccept(key);
                }
                //2.如果是数据通道有数据来了,
                if (key.isReadable()) {
                    log.info("处理读入事件");
                    handleRead(key);
                }
                //3.通道可写
                if (key.isWritable() && key.isValid()) {
                    log.info("处理写出事件");
                    handleWrite(key);
                }
                iterator.remove();
            }
            selectCount++;
        }
    }

    @SneakyThrows
    private void handleRead(SelectionKey key) {

        SocketChannel dataChannel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //读入网络数据
        dataChannel.read(byteBuffer);

        new Thread(() -> {
            //这里读数据,由于数据是从kennel得到 当前byte Buffer
            // 所以会短暂阻塞当前线程
            //相当于业务线程处理业务数据
            byteBuffer.flip();
            log.info("服务端收到的数据为:{}", new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit()), StandardCharsets.UTF_8);
            byteBuffer.clear();


            //准备回送数据。
            ByteBuffer sendBuffer = ByteBuffer.wrap("来自服务端的问候".getBytes(StandardCharsets.UTF_8));
            //注册可写事件,
            // 问:为毛不直接发送数据?
            // 答:主要是发送数据这种事情不要来占用业务线程的时间,收数发据还是留给IO线程,当然直接发送也是可以的,写这个操作不会怎么阻塞线程。毕竟是内存之间复制数据嘛。
            try {
                dataChannel.register(key.selector(), SelectionKey.OP_WRITE, sendBuffer);
                log.info("注册可写事件成功");
            } catch (ClosedChannelException e) {
                log.info("注册可写事件失败", e);
            }
        }, "业务线程-" + threadNameIndex++).start();
    }

    private void handleAccept(SelectionKey key) {
        //连接通道收到连接请求了。
        ServerSocketChannel acceptChannel = (ServerSocketChannel) key.channel();
        try {
            //建立连接
            SocketChannel dataChannel = acceptChannel.accept();
            dataChannel.configureBlocking(false);
            //继续将数建立好的数据通道注册至选择器 Selector,等待数据的来临
            dataChannel.register(key.selector(), SelectionKey.OP_READ);
        } catch (IOException e) {
            log.info("连接失败", e);
        }
    }

    @SneakyThrows
    private void handleWrite(SelectionKey key) {

        SocketChannel dataChannel = (SocketChannel) key.channel();
        ByteBuffer byteBuffer = (ByteBuffer) key.attachment();

        //发送数据至客户端,
        // 当前bytebuffer --> kennel bytebuffer 段,
        // 短暂阻塞当前线程
        log.info("发送数据为={}", new String(byteBuffer.array(), byteBuffer.position(), byteBuffer.limit(), StandardCharsets.UTF_8));
        dataChannel.write(byteBuffer);
        log.info("已经向客户端发送数据");
        dataChannel.close();
        byteBuffer.clear();
    }


    @SneakyThrows
    @Test
    public void testClient() {

        Socket socket = new Socket();
        socket.connect(new InetSocketAddress("localhost", 3301), 2000);
        log.info("连接上服务器了,开始发送数据");
        //发送数据给服务端
        PrintWriter printWriter = new PrintWriter(socket.getOutputStream(), true);
        printWriter.print("大王叫我来巡山,");
        printWriter.print("巡到一个花果山,");
        printWriter.print("是不是想当王,是不是想登基");
        printWriter.flush();

        //接收服务端数据
        byte[] datas = new byte[256];
        socket.getInputStream().read(datas);
        log.info("客户端收到的数据={}", new String(datas, StandardCharsets.UTF_8));
        socket.close();
    }
}

上述代码,我们建立了一个简单的NioServer, 客户端即用testClient方法,客户端保持使用bio, 我们主要研究服务端nio 过程。在其处理过程中,我们打印日志,用以跟踪其处理流程。如:

14:55:45.412 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer -1次轮询
14:55:45.419 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer -1次轮询的第1次轮key,Key数量=1
14:55:45.419 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 处理连接事件
14:55:45.420 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer -2次轮询
14:55:45.420 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer -2次轮询的第1次轮key,Key数量=1
14:55:45.420 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 处理读入事件
14:55:45.422 [业务线程-0] INFO xyd.tan.yongfu.sb2.nio.NioServer - 服务端收到的数据为:大王叫我来巡山,巡到一个花果山,是不是想当王,是不是想登基
14:55:45.423 [业务线程-0] INFO xyd.tan.yongfu.sb2.nio.NioServer - 注册可写事件成功
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer -3次轮询
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer -3次轮询的第1次轮key,Key数量=1
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 处理写出事件
14:55:45.437 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 发送数据为=来自服务端的问候
14:55:45.438 [main] INFO xyd.tan.yongfu.sb2.nio.NioServer - 已经向客户端发送数据

过程分析:

  • 第一个循环:处理的是连接事件,并在其中注册了一个读事件。
  • 第二个循环:处理读事件,将数据读入后,委托给另外一个业务线程进行业务处理(正式的,会使用专门的业务线程池。),并在业务线程中,处理完业务以后,注册写事件。
  • 第三个循环:处理写事件,将数据发送至客户端。

网络NIO与网络BIO对比:
bio,模型为:连接线程获取到连接请求以后,创建连接,然后将连接委托给业务线程。此时,业务线程需要阻塞并等待网络数据的到来,然后处理完业务,再将数据复制到socket 缓存中。此过程,业务线程被阻塞在网络数据的传输中,浪费了线程利用率。特别在高并发的时候,很多连接无法分配到线程,导致业务无法处理。

nio,模型为:Selector所在的master线程一直循环检测 Selector中注册的的通道是件是否产生事件。如果产生了事件,就会取出相应的通道,并对事件进行处理。
具体的:

  • 对于连接事件,master线程直接创建连接,并注册读事件。
  • 对于通道读事件,由于数据到达kennel buffer后才会产生读事件,所以master线程可直接读取数据,这个过程,由于是kennel buffer与user buffer 之间数据复制,不存在等待网络数据传输,所以阻塞时间很短。数据获取以后,可直接将数据委托给业务线程业务线程不会处理IO事件,故不会阻塞和浪费业务线程资源。
  • 对于通道写事件,由于是从user buffer 复制数据至kennel buffer中,不涉及网络慢io,故业务线程可以处理, master线程也可处理,对性能影响不大。

总结:
对于两个不同的io模式, nio 的主要优点是释放业务线程的职能,让业务线程不用一直去阻塞等待网络数据的传输。将其精力主要放在处理后端业务能力上面,以期能够承载更多的计算任务。nio模型主要是通过Selector 内部的事件模型,避免业务线程的等待,从而在相同的时间内能够容纳更多的网络连接。nio的优点是能够容纳更多的计算业务和网络连接,对于单个客户端连接的服务,并不会提高其响应速度

AIO

aio,即Asynchronous IO,异步io。典型的异步处理模式,同步发出请求,返回处理中,线程释放回线程池,接收其他任务。异步通知回来,启用新线程进行处理回调任务。故在调用异步IO的时候,需要传递一个 CompletionHandler<V,A>回调函数,用于kennel处理完io以后,使用新线程处理回调函数里面的任务。

在这里插入图片描述

以下是一个AIO File 的事例:

@Slf4j
public class AioFileChannelTest {

    @SneakyThrows
    @Test
    public void testAioFile() {

        //创建异步处理通道,简单演示,d:\\a.txt内容不要超过1024字节。
        Path path = Paths.get("d:", "a.txt");
        AsynchronousFileChannel afc = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
        //创建一个内存缓存块。
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);

        //异步读取文件内容,并传递一个回调函数
        log.info("发送读取请求时当前线程名称:{}", Thread.currentThread().getName());
        afc.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
            @SneakyThrows
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                log.info("回调函数开始执行,已经读取完文件数据,线程名:{}", Thread.currentThread().getName());
                attachment.flip();
                log.info("文件内容为:{}" + new String(attachment.array(), attachment.position(), attachment.limit(), StandardCharsets.UTF_8));
                //处理完数据,关闭通道
                afc.close();
            }
            @SneakyThrows
            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                afc.close();
            }
        });
    }
}

请看日志,特别注意其执行的线程。

17:09:06.428 [main] INFO xyd.tan.yongfu.sb2.aio.AioFileChannelTest - 发送读取请求时当前线程名称:main
17:09:06.434 [Thread-13] INFO xyd.tan.yongfu.sb2.aio.AioFileChannelTest - 回调函数开始执行,已经读取完文件数据,线程名:Thread-13
17:09:06.434 [Thread-13] INFO xyd.tan.yongfu.sb2.aio.AioFileChannelTest - 文件内容为:大王叫我来巡山,巡到一个火焰山。你是真的秀!

再来一个,关于AIO server。

public class AioServer {

    AsynchronousServerSocketChannel assc;


    @SneakyThrows
   public  void start(){
        //1. 创建一个线程池
        ExecutorService es = Executors.newCachedThreadPool();

        //2. 创建异步通道群组,并设置线程池。
        AsynchronousChannelGroup tg = AsynchronousChannelGroup.withCachedThreadPool(es, 1);

        //3. 创建服务端异步通道
        assc = AsynchronousServerSocketChannel.open(tg);

        //4. 绑定监听端口
        assc.bind(new InetSocketAddress(80));

        AioAttachment aioAttachment = new AioAttachment();
        aioAttachment.setServer(assc);

        //5. 监听连接,传入回调类处理连接请求
        assc.accept(aioAttachment, new AcceptHandler());
    }
}

定义 在各个线程之间传输的上下文附件。


@Data
public class AioAttachment {

    private AsynchronousServerSocketChannel server;

    private AsynchronousSocketChannel client;

    private boolean isReadMode;

    private ByteBuffer buffer;

    private WriteCompletHander writeCompletHander;

    private ReadCompletHander readCompletHander;

}

连接完成处理器。

@Slf4j
public class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, AioAttachment> {

    @SneakyThrows
    @Override
    public void completed(AsynchronousSocketChannel channel, AioAttachment attachment) {
        //继续监听
        attachment.getServer().accept(attachment, this);
        log.info("收到连接请求={}", channel.getRemoteAddress());
        //
        AioAttachment aioAttachment = new AioAttachment();
        aioAttachment.setServer(attachment.getServer());
        aioAttachment.setClient(channel);
        aioAttachment.setReadMode(true);
        aioAttachment.setBuffer(ByteBuffer.allocate(1024 << 4));
        aioAttachment.setWriteCompletHander(new WriteCompletHander());
        aioAttachment.setReadCompletHander(new ReadCompletHander());

        //连接成功后,马上发起读取数据请求。
        channel.read(aioAttachment.getBuffer(), aioAttachment, aioAttachment.getReadCompletHander());
    }

    @Override
    public void failed(Throwable exc, AioAttachment attachment) {
        log.info("处理连接请求失败={}", exc);
    }
}

数据读取完成处理器

@Slf4j
public class ReadCompletHander implements CompletionHandler<Integer, AioAttachment> {

    @SneakyThrows
    @Override
    public void completed(Integer result, AioAttachment attachment) {

        // 读取来自客户端的数据
        ByteBuffer buffer = attachment.getBuffer();
        buffer.flip();
        byte bytes[] = new byte[buffer.limit() - buffer.position()];
        buffer.get(bytes);
        String msg = new String(bytes, StandardCharsets.UTF_8).trim();
        log.info("收到来自客户端的数据: " + msg);

        // 响应客户端请求,准备回写的数据
        buffer.clear();
        buffer.put(("Response from server!:" + RandomStringUtils.randomAlphanumeric(10)).getBytes(StandardCharsets.UTF_8));
        // 写数据到客户端也是异步
        buffer.flip();
        log.info("发送数据至客户端");
        attachment.getClient().write(buffer, attachment, attachment.getWriteCompletHander());
    }

    @Override
    public void failed(Throwable exc, AioAttachment attachment) {
        log.info("读取数据失败", exc);
    }
}

数据写入完成处理器


@Slf4j
public class WriteCompletHander implements CompletionHandler<Integer, AioAttachment> {

    @SneakyThrows
    @Override
    public void completed(Integer result, AioAttachment attachment) {
        // 继续监听读取事件。
        log.info("数据发送完毕,继续监听");
        attachment.getBuffer().clear();
        attachment.getClient().read(attachment.getBuffer(), attachment, attachment.getReadCompletHander());
    }

    @Override
    public void failed(Throwable exc, AioAttachment attachment) {
        log.info("写数据失败", exc);
    }
}

总结:
IO中的几种访问方式,如缓存IO,直接IO,内存映射IO,其反映的是数据流的交互方式,其主要影响IO响应速度,各自有其优缺点。平时在使用相应的IO框架的时候,可以了解其底层实现逻辑,以判定其是否合适当前场景。

java 中的 bio ,nio ,aio ,其核心差异并不是提高IO的响应速度,而是通过使用操作系统提供的能力,尽量减少业务线程去等待和处理IO流的相关事宜。将业务线程尽可能释放出来,以方便系统可以处理更多的业务。从而在宏观上面提升系统的吞吐量。

参考资料
Java 文件 IO 操作之 DirectIO
Java IO模型–边学边写边理解
Java NIO 之 Channel(通道)
Java NIO 之 Buffer(缓冲区)
Java NIO之Selector(选择器)

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值