漫谈I/O——异步非阻塞I/O

131 阅读4分钟

参考

Linux 网络包发送过程:25 张图,一万字,拆解 Linux 网络包发送过程 (qq.com)

图解Linux网络包接收过程: 图解Linux网络包接收过程 (qq.com)

当内核收到了一个网络包:当内核收到了一个网络包 (qq.com)

epoll 是如何实现 IO 多路复用的:图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的! (qq.com)

你管这破玩意叫 IO 多路复用: 你管这破玩意叫 IO 多路复用? (qq.com)

认认真真的聊聊中断: 认认真真的聊聊中断 (qq.com)

认认真真的聊聊"软"中断:认认真真的聊聊"软"中断 (qq.com)

【视频】netty视频

【书】计算网络自顶向下

【书】UNIX网络编程卷1:套接字联网API(第三版)

【书】图解TCP_IP

本文笔者只是做整合以及阅读总结,建议大家看看原文## 参考

Linux 网络包发送过程:25 张图,一万字,拆解 Linux 网络包发送过程 (qq.com)

图解Linux网络包接收过程: 图解Linux网络包接收过程 (qq.com)

当内核收到了一个网络包:当内核收到了一个网络包 (qq.com)

epoll 是如何实现 IO 多路复用的:图解 | 深入揭秘 epoll 是如何实现 IO 多路复用的! (qq.com)

你管这破玩意叫 IO 多路复用: 你管这破玩意叫 IO 多路复用? (qq.com)

认认真真的聊聊中断: 认认真真的聊聊中断 (qq.com)

认认真真的聊聊"软"中断:认认真真的聊聊"软"中断 (qq.com)

【视频】netty视频

【书】计算网络自顶向下

【书】UNIX网络编程卷1:套接字联网API(第三版)

【书】图解TCP_IP

本文笔者只是做整合以及阅读总结,建议大家看看原文

异步非阻塞I/O

顾名思义就是异步比如read事件,你只需要提共一个回调函数给read函数,那么它就会再读到数据之后直接调用用户回调函数。

AIO 用来解决数据复制阶段的阻塞问题

  • 同步意味着,在进行读写操作时,线程需要等待结果,还是相当于闲置
  • 异步意味着,在进行读写操作时,线程不必等待结果,而是将来由操作系统来通过回调方式由另外的线程来获得结果

异步模型需要底层操作系统(Kernel)提供支持

  • Windows 系统通过 IOCP 实现了真正的异步 IO
  • Linux 系统异步 IO 在 2.6 版本引入,但其底层实现还是用多路复用模拟了异步 IO,性能没有优势

网络AIO

先来看看 AsynchronousFileChannel

public class AioServer {
    public static void main(String[] args) throws IOException {
        AsynchronousServerSocketChannel ssc = AsynchronousServerSocketChannel.open();
        ssc.bind(new InetSocketAddress(8080));
        ssc.accept(null, new AcceptHandler(ssc));
        System.in.read();
    }
​
    private static void closeChannel(AsynchronousSocketChannel sc) {
        try {
            System.out.printf("[%s] %s close\n", Thread.currentThread().getName(), sc.getRemoteAddress());
            sc.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
​
    private static class ReadHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel sc;
​
        public ReadHandler(AsynchronousSocketChannel sc) {
            this.sc = sc;
        }
​
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            try {
                if (result == -1) {
                    closeChannel(sc);
                    return;
                }
                System.out.printf("[%s] %s read\n", Thread.currentThread().getName(), sc.getRemoteAddress());
                attachment.flip();
                System.out.println(Charset.defaultCharset().decode(attachment));
                attachment.clear();
                // 处理完第一个 read 时,需要再次调用 read 方法来处理下一个 read 事件
                sc.read(attachment, attachment, this);
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
​
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            closeChannel(sc);
            exc.printStackTrace();
        }
    }
​
    private static class WriteHandler implements CompletionHandler<Integer, ByteBuffer> {
        private final AsynchronousSocketChannel sc;
​
        private WriteHandler(AsynchronousSocketChannel sc) {
            this.sc = sc;
        }
​
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            // 如果作为附件的 buffer 还有内容,需要再次 write 写出剩余内容
            if (attachment.hasRemaining()) {
                sc.write(attachment);
            }
        }
​
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            exc.printStackTrace();
            closeChannel(sc);
        }
    }
​
    private static class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> {
        private final AsynchronousServerSocketChannel ssc;
​
        public AcceptHandler(AsynchronousServerSocketChannel ssc) {
            this.ssc = ssc;
        }
​
        @Override
        public void completed(AsynchronousSocketChannel sc, Object attachment) {
            try {
                System.out.printf("[%s] %s connected\n", Thread.currentThread().getName(), sc.getRemoteAddress());
            } catch (IOException e) {
                e.printStackTrace();
            }
            ByteBuffer buffer = ByteBuffer.allocate(16);
            // 读事件由 ReadHandler 处理
            sc.read(buffer, buffer, new ReadHandler(sc));
            // 写事件由 WriteHandler 处理
            sc.write(Charset.defaultCharset().encode("server hello!"), ByteBuffer.allocate(16), new WriteHandler(sc));
            // 处理完第一个 accpet 时,需要再次调用 accept 方法来处理下一个 accept 事件
            ssc.accept(null, this);
        }
​
        @Override
        public void failed(Throwable exc, Object attachment) {
            exc.printStackTrace();
        }
    }
}

可以看到

  • 响应文件读取成功的是另一个线程 Thread-5
  • 主线程并没有 IO 操作阻塞

总结

此小姐我们总结了4中I/O模式并逐个分析了优缺点,但其实还有一个信号驱动模式这里没有提及,想要了解的可以直接去看看《UNIX网络编程》这本书的第25章。从I/O演进来看其实每一中都是上一种IO的模式的迭代和改进,epoll和IOCP也不会是终点。此外虽然上层在疯狂变化但是底层仍然是TCP/IP,epoll的关键仍然有红黑树这种数据结构。可见这些基础的重要性,这是不变的东西。当你把另外一项技术研究到底的时候,你迟早会和他们再次相遇。