Java BIO NIO 与 AIO 分析第三部分之AIO

原创文章, 转载请私信. 订阅号 tastejava 回复 aio思维导图获取思维导图源文件

AIO部分

上一篇文章中分析了BIO部分, 接下来分析一下AIO部分, AIO是JDK1.7新增的属于java.nio包下的IO组件. 还是一样的思路, 从了解AIO的各个重要组件开始. JAVA的AIO只提供TCP操作和文件操作, 没有提供UDP支持. 主要组件有CompletionHandler, AsynchronousFileChannel, AsynchronousServerSocketChannel, AsynchronousSocketChannel 共四个组件, 各自作用为:

  1. CompletionHandler 包含了异步IO通用回调方法
  2. AsynchronousFileChannel 用于操作文件
  3. AsynchronousServerSocketChannel 用于操作TCP连接, 监听服务器端
  4. AsynchronousSocketChannel 用于操作TCP连接

AIO与NIO差别

AIO全称是非阻塞异步IO(Asynchronous IO), 真正的实现了IO操作的异步处理.与NIO的轮询连接是否处于就绪状态不同, AIO是传入一个回调方法和缓存参数, 读取或者发送数据操作交由操作系统实现, 当IO操作完成后, 相应的AsynchronousChannel会回调提供的回调方法, 在回调方法中进行数据获取以及业务处理.

AIO NIO与IO多路复用的关系

AIO和NIO都属于IO多路复用的实现方式, AIO与NIO都实现了IO多路复用. AIO属于proactor模式, NIO属于reactor模式. 具体什么是reactor, 什么是reactor在之后的文章再仔细分析, 这篇文章只是分析Java提供的AIO本身.

AIO体系结构

AIO体系结构
从图中可以看到AIO体系非常简洁, 下面我们一个一个组件梳理, 分析怎么使用.

CompletionHandler 分析

在正式分析文件或者网络的异步IO操作前, 首先要熟悉一个很重要的接口, CompletionHandler, 这个接口作用是什么呢, 一句话概括, CompletionHandler用于消费处理异步IO操作产生的结果数据.源码原始注释如下:

A handler for consuming the result of an asynchronous I/O operation.

接口签名为

public interface CompletionHandler<V,A>

这里的类型参数V和A源码注释描述的也十分简洁:

 * @param   <V>     The result type of the I/O operation
 * @param   <A>     The type of the object attached to the I/O operation

即V是异步IO操作返回的结果, A是调用IO操作时额外附加的对象.
CompletionHandler提供的回调方法总共有两个, 一个是IO操作完成并且成功回调的completed方法, 一个是IO操作失败后回调的failed方法.详细源码和分析如下:

    /**
     * Invoked when an operation has completed.
     * 当IO操作成功完成后调用此方法
     *
     * @param   result
     *          The result of the I/O operation.
     * 			异步IO操作产生的结果
     * @param   attachment
     *          The object attached to the I/O operation when it was initiated.
     * 			调用异步IO操作时初始化的附加对象
     */
    void completed(V result, A attachment);

    /**
     * Invoked when an operation fails.
     * 当IO操作失败时的回调方法
     *
     * @param   exc
     *          The exception to indicate why the I/O operation failed
     * 			IO操作产生的错误对象
     * @param   attachment
     *          The object attached to the I/O operation when it was initiated.
     * 			调用异步IO操作时初始化的附加对象
     */
    void failed(Throwable exc, A attachment);

理解具体AIO的前置条件是要记住CompletionHandler两个类型参数, 第一个V是IO操作结果, 第二个A是附加对象, 接下来具体看看AIO的文件操作和网络TCP操作.

AsynchronousFileChannel 分析

用AIO读取文件, 核心过程是将读取这个操作实际执行者委托给操作系统, 操作系统将数据读到传递的缓存对象中, 然后回调读取成功方法.异步IO操作都分为两种, 一种是返回一个Future对象, 这种方式的get方法是阻塞的, 虽然用的是异步IO操作, 但是实际还是同步非阻塞IO, 还是reactor模式. 另一种是传递一个回调函数, 异步IO操作完成后自动回调传递的回调函数, 这一种是真正的异步IO, 是proactor模式.
下面分别用同步非阻塞和异步非阻塞的方式操作文件, 具体代码如下:
reactor模式

// 虽然用的是异步IO, 但是还是reactor模式的IO多路复用
@Test
public void testAsynchronousFileChannelWidthFuture() throws IOException, ExecutionException, InterruptedException {
    // 分配字节缓存
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    String filePath = getClass().getResource("/") + "/SimpleReadFile.txt";
    Path path = Paths.get(filePath.replaceFirst("file:/", ""));
    // 获得异步通道实例
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
    Future<Integer> read = asynchronousFileChannel.read(byteBuffer, 0);
    // 判断future是否完成读取, 未完成时当前线程可以处理其他业务
    while (!read.isDone()) {
        // 其他业务逻辑
        continue;
    }
    // Future的get方法是阻塞方法, 未完成会阻塞到完成并且返回结果.
    Integer readCount = read.get();
    byteBuffer.flip();
    CharBuffer charBuffer = Charsets.UTF_8.decode(byteBuffer);
    int limit = charBuffer.limit();
    char[] chars = new char[limit];
    charBuffer.get(chars);
    log.info("读取了{}个字节", readCount);
    log.info("{}", String.valueOf(chars));
}

proactor模式

// 真正的异步IO即AIO, 使用proactor模式
@Test
public void testAsynchronousFileChannelWidthCompletionHandler() throws IOException, InterruptedException {
    String filePath = getClass().getResource("/") + "/SimpleReadFile.txt";
    Path path = Paths.get(filePath.replaceFirst("file:/", ""));
    AsynchronousFileChannel asynchronousFileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ);
    ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024);
    asynchronousFileChannel.read(byteBuffer, 0, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
        // 读取完成后回调方法
        @Override
        public void completed(Integer result, ByteBuffer attachment) {
            attachment.flip();
            CharBuffer charBuffer = Charsets.UTF_8.decode(attachment);
            int limit = charBuffer.limit();
            char[] chars = new char[limit];
            charBuffer.get(chars);
            log.info("读取了{}个字节", result);
            log.info("{}", String.valueOf(chars));
        }
        // 读取过程失败回调方法
        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            log.info("读取出错");
        }
    });
    Thread.sleep(2000);
}

异步TCP IO操作分析

AIO只支持TCP, 不提供UDP支持, 接下来看一下AsynchronousServerSocketChannel和AsynchronousSocketChannel的具体使用.网络的异步IO操作时, 返回结果(包括AsynchronousServerSocketChannel的accept, AsynchronousSocketChannel.connect方法返回结果)都有返回Future和传入CompletionHandler回调方法两种方式, 返回Future的方法是换了个地方阻塞, 即使用isDone判断Future是否完成, 也是轮询的方式, 不做描述, 接下来的示例代码都是通过回调的方法使用AIO.

首先准备一个工具方法, 作用是将字符串数据放入Buffer, 供网络传输数据使用.

// 将字符串放入指定Buffer对象的工具方法
private ByteBuffer getReadableByteBufferWithData(ByteBuffer byteBuffer, String message) {
    byteBuffer.put(message.getBytes());
    // 切换字节缓存为读状态, 为发送数据做准备
    byteBuffer.flip();
    return byteBuffer;
}

AIO网络操作主要流程如下:
①网络操作的发送数据流程主要是将数据写入Buffer, 然后调用AsynchronousSocketChannel的write方法, 并将完成操作的回调方法类实例CompletionHandler传进去.
②网络操作的接收数据流程主要是调用AsynchronousSocketChannel的read方法, 传入缓存Buffer对象, 传入回调方法实例CompletionHandler, 当接收数据完成后, read方法会自动回调传入的回调方法.
可以看到, 不管接收数据还是发送数据, 都要提供回调方法CompletionHandler, 所以我们还要准备一个接收数据完成的回调CompletionHandler类实例, 和一个发送数据完成的回调CompletionHandler类实例.具体代码如下:

/**
 * @author GaoZl
 * @date 2019/12/3 9:46
 * 发送数据完成后回调函数
 */
@Slf4j
public class SendCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // 发送成功, 清空缓存并切换为写状态
        attachment.clear();
        log.info("状态: 发送消息完成");
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        log.info("状态: 发送消息失败");
    }
}
/**
 * @author GaoZl
 * @date 2019/12/3 9:45
 * 接收数据完成后的回调函数
 */
@Slf4j
public class ReceiveCompletionHandler implements CompletionHandler<Integer, ByteBuffer> {
    private AsynchronousSocketChannel asynchronousSocketChannel;

    public ReceiveCompletionHandler(AsynchronousSocketChannel asynchronousSocketChannel) {
        this.asynchronousSocketChannel = asynchronousSocketChannel;
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // 切换缓存为读状态
        attachment.flip();
        CharBuffer charBuffer = Charsets.UTF_8.decode(attachment);
        // 读取数据完成, 清空缓存并切换为写状态
        attachment.clear();
        // 本条消息数据已经读取到了 charBuffer
        // 在处理数据同时未完成处理的时候有数据传入会存在数据丢失的情况, 还需解决
        asynchronousSocketChannel.read(attachment, attachment, this);
        // 数据的业务处理逻辑
        int limit = charBuffer.limit();
        char[] chars = new char[limit];
        charBuffer.get(chars);
        log.info("读取了{}个字节", result);
        log.info("接收到消息: {}", String.valueOf(chars));
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        log.info("状态: 接收数据失败");
    }
}

有了具体的接收数据回调类和发送数据回调类后, 就是具体组合使用了, 下面依旧是从实际代码和注释来看一下AIO的具体TCP网络操作, 代码如下:

// 服务器端
@Test
public void testAsynchronousServerSocketChannel() throws IOException, InterruptedException {
    ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    AsynchronousServerSocketChannel asynchronousServerSocketChannel = AsynchronousServerSocketChannel.open();
    asynchronousServerSocketChannel.bind(new InetSocketAddress(9999));
    asynchronousServerSocketChannel.accept(writeBuffer, new CompletionHandler<AsynchronousSocketChannel, ByteBuffer>() {
        @Override
        public void completed(AsynchronousSocketChannel result, ByteBuffer attachment) {
            // 发送消息
            // 发送数据完成后回调函数
            SendCompletionHandler sendCompletionHandler = new SendCompletionHandler();
            // 发送数据
            getReadableByteBufferWithData(writeBuffer, "消息来自服务器, 连接已经建立");
            result.write(writeBuffer, writeBuffer, sendCompletionHandler);
            // 接收消息
            ReceiveCompletionHandler readCompletionHandler = new ReceiveCompletionHandler(result);
            result.read(readBuffer, readBuffer, readCompletionHandler);
        }

        @Override
        public void failed(Throwable exc, ByteBuffer attachment) {
            log.info("状态: 服务器与客户端建立连接失败");
        }
    });
    // 主线程阻塞
    Thread.sleep(1000 * 3600);
}
// 客户端
@Test
public void testAsynchronousSocketChannel() throws IOException, InterruptedException {
    ByteBuffer readBuffer = ByteBuffer.allocateDirect(1024);
    ByteBuffer writeBuffer = ByteBuffer.allocateDirect(1024);
    AsynchronousSocketChannel asynchronousSocketChannel = AsynchronousSocketChannel.open();
    asynchronousSocketChannel.connect(new InetSocketAddress("127.0.0.1", 9999), asynchronousSocketChannel,
            new CompletionHandler<Void, AsynchronousSocketChannel>() {
                @Override
                public void completed(Void result, AsynchronousSocketChannel attachment) {
                    // 接收消息
                    ReceiveCompletionHandler readCompletionHandler = new ReceiveCompletionHandler(attachment);
                    attachment.read(readBuffer, readBuffer, readCompletionHandler);
                    // 5秒向客户端发送一次消息
                    while (true) {
                        try {
                            Thread.sleep(5000);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        // 发送数据完成后回调函数
                        SendCompletionHandler sendCompletionHandler = new SendCompletionHandler();
                        getReadableByteBufferWithData(writeBuffer, "消息来自客户端, 连接已经建立");
                        asynchronousSocketChannel.write(writeBuffer, writeBuffer, sendCompletionHandler);
                    }
                }

                @Override
                public void failed(Throwable exc, AsynchronousSocketChannel attachment) {
                    log.info("状态: 客户端与服务器建立连接失败");
                }
            });
    // 主线程阻塞
    Thread.sleep(1000 * 3600);
}

总结

AIO和NIO带来的性能提升是里程碑式的, 但是API设计比较繁琐, Buffer的flip设计也不是很好用, 网络编程还是建议netty这种成熟的网络框架.

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值