Netty 学习笔记(三)、AIO 简单介绍

概述

上篇博客我简单介绍了 NIO 相关知识。JDK 1.7 版本对 NIO 库做了升级,实现了完全异步通信的新 IO 套接字以及通道,这种新 NIO 也被称为 AIO,本篇博客我就来简单介绍 AIO 相关知识。


AIO 示例

AIO 和 NIO 相比,最大的改进在于支持真正意义上的异步非阻塞。因为大部分实现和 NIO 相同,本篇我计划通过 AIO 实现时间服务器,并对关键 API 做简要说明。下面我们直接看代码,代码中每一行代码的功能我通过注释的方式给出:

AIO 服务端:

// aio 服务端启动类
public class AioServer implements Runnable {

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

    private static final int PORT = 8888;

    /**
     * AIO 服务端套接字
     */
    AsynchronousServerSocketChannel socketChannel = null;

    /**
     * 同步锁
     */
    CountDownLatch latch;


    public AioServer() {
        try {
            // 创建服务端套接字
            socketChannel = AsynchronousServerSocketChannel.open();
            // 服务端绑定端口
            socketChannel.bind(new InetSocketAddress(PORT));
            System.out.println("AIO server start in port : " + PORT);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void run() {
        // 初始化倒计时锁存
        latch = new CountDownLatch(1);
        // 接受请求并处理方法
        doAccept();
        try {
            // 使当前线程阻塞,除非 latch 对象调用1次 countDown() 方法
            // 这里必须先阻塞着,不然 doAccept() 方法中异步调用没返回前,由于没有后续操作,服务端会停止
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    private void doAccept() {
        // 异步接受客户端连接,我们通过 CompletionHandler 类型的 handler 处理连接成功的事件
        // 这里 this 表示异步回调方法中的参数
        socketChannel.accept(this, new AioServerHandler());
    }
}
// 主要进行异步连接以及读写操作的异步调用
public class AioServerHandler implements CompletionHandler<AsynchronousSocketChannel, AioServer> {

    @Override
    public void completed(AsynchronousSocketChannel result, AioServer attachment) {
        // 连接成功时再次调用 accept() 方法,表示等待其它客户端连接
        // 也是是说,每成功连接一个客户端,再异步调用该方法等待下一个客户端连接
        attachment.socketChannel.accept(attachment, this);
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        // 此时通过 read() 方法进行异步读取
        // 其中三个参数分别表示,从 channel 读取数据的缓冲区、回调方法参数中的缓冲区,和具体的回调实现类
        result.read(buffer, buffer, new AioServerChannelHandler(result));
    }

    @Override
    public void failed(Throwable exc, AioServer attachment) {
        // 连接失败时,通过调用 countDown() 结束线程
        attachment.latch.countDown();
    }
}
// 主要进行异步读写处理
public class AioServerChannelHandler implements CompletionHandler<Integer, ByteBuffer> {

    private AsynchronousSocketChannel channel;

    public AioServerChannelHandler(AsynchronousSocketChannel channel) {
        this.channel = channel;
    }

    @Override
    public void completed(Integer result, ByteBuffer attachment) {
        // 将缓冲区切换为读模式
        attachment.flip();
        // 获取缓冲区可读的数据长度
        byte[] bytes = new byte[attachment.remaining()];
        // 将缓冲区中可读数据读取到字节数组
        attachment.get(bytes);
        try {
            // 解码消息内容
            String message = new String(bytes, "UTF-8");
            System.out.println("This time server receive message: " + message);
            // 根据消息内容设置返回值
            String resultMessage = message.equalsIgnoreCase("QUERY TIME ORDER")
                    ? new Date(System.currentTimeMillis()).toString() : "I don't know";
            // 异步返回数据
            doWrite(resultMessage);
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }
    }

    private void doWrite(String message) {
        if (message != null && message.trim().length() > 0) {
            byte[] bytes = message.getBytes();
            ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
            // 将要返回给客户端的数据写入缓冲区
            buffer.put(bytes);
            // 从写模式切换到读模式,确定范围
            buffer.flip();
            // 异步发送数据给客户端
            channel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
                @Override
                public void completed(Integer result, ByteBuffer attachment) {
                    // 判断缓冲区中是否残留还未发送的数据
                    if (attachment.hasRemaining()) {
                        // 继续发送
                        channel.write(attachment, attachment, this);
                    }
                }

                @Override
                public void failed(Throwable exc, ByteBuffer attachment) {
                    // 异步发送数据给客户端失败
                    try {
                        channel.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
        // 异步读数据到缓冲区失败
        try {
            this.channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

AIO 客户端

public class AioClient implements Runnable, CompletionHandler<Void, AioClient> {

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

    private static final String ADDRESS = "127.0.0.1";

    private static final int PORT = 8888;

    /**
     * AIO 客户端套接字
     */
    private AsynchronousSocketChannel channel;

    /**
     * 同步锁
     */
    private CountDownLatch latch;

    public AioClient() {
        try {
            // 初始化客户端套接字
            channel = AsynchronousSocketChannel.open();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
    
    @Override
    public void run() {
        // 初始化倒计时锁存
        latch = new CountDownLatch(1);
        // 客户端采用异步连接的方式,其中异步回调类就是本身,并且回调类方法参数也是本身
        channel.connect(new InetSocketAddress(ADDRESS, PORT), this, this);
        try {
            // 使当前线程阻塞,除非 latch 对象调用1次 countDown() 方法
            latch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 代码通过 await() 方法时,表示客户端也该停止了
        try {
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void completed(Void result, AioClient attachment) {
        // 此时表示异步连接成功,开始向服务端发送消息
        byte[] bytes = "QUERY TIME ORDER".getBytes();
        ByteBuffer buffer = ByteBuffer.allocate(bytes.length);
        buffer.put(bytes);
        buffer.flip();
        channel.write(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
            @Override
            public void completed(Integer result, ByteBuffer attachment) {
                // 判断缓冲区中是否还存在未发送完的数据
                if (attachment.hasRemaining()) {
                    // 没发送完就继续发
                    channel.write(attachment, attachment, this);
                } else {
                    // 发送完毕后,异步等待服务端返回
                    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
                    channel.read(byteBuffer, byteBuffer, new CompletionHandler<Integer, ByteBuffer>() {
                        @Override
                        public void completed(Integer result, ByteBuffer attachment) {
                            // 此时表示服务端返回数据已读取完毕
                            attachment.flip();
                            byte[] message = new byte[attachment.remaining()];
                            attachment.get(message);
                            String body = null;
                            try {
                                body = new String(message, "UTF-8");
                                System.out.println("receive message from Server:" + body);
                            } catch (UnsupportedEncodingException e) {
                                e.printStackTrace();
                            } finally {
                                latch.countDown();
                            }
                        }

                        @Override
                        public void failed(Throwable exc, ByteBuffer attachment) {
                            // 服务端返回数据读取失败
                            try {
                                channel.close();
                            } catch (IOException e) {
                                e.printStackTrace();
                            } finally {
                                latch.countDown();
                            }
                        }
                    });
                }
            }

            @Override
            public void failed(Throwable exc, ByteBuffer attachment) {
                // 异步发送数据失败
                try {
                    channel.close();
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    latch.countDown();
                }
            }
        });
    }

    @Override
    public void failed(Throwable exc, AioClient attachment) {
        // 异步连接服务端失败
        exc.printStackTrace();
        try {
            channel.close();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            latch.countDown();
        }
    }
}

运行结果

服务端:
AIO server start in port : 8888
This time server receive message: QUERY TIME ORDER

客户端:
receive message from Server:Mon Oct 12 19:39:03 CST 2020

AIO 常用 API 介绍

上述代码中注释已非常详细,这里我简要补充几点:

CompletionHandler 接口:

示例代码中所有异步回调逻辑都通过 CompletionHandler 接口实现,其中该接口是这样定义的:

public interface CompletionHandler<V,A>
  • V : I/O 操作的结果类型
  • A : 附加到 I/O 操作的对象类型

该接口主要用于消耗 I/O 操作的异步结果,其中实现该接口需要实现以下两种方法:

void completed​(V result, A attachment):操作完成时调用
void failed​(Throwable exc, A attachment):操作失败时调用

其中该接口一般是这样使用的:

// 异步非阻塞等待连接,attachment 表示处理器对象回调方法的参数对象,handler 表示处理器对象
accept(A attachment, CompletionHandler<AsynchronousSocketChannel,? super A> handler);
// 异步非阻塞写,src 表示数据源缓冲池,attachment ...
write(ByteBuffer src, A attachment, CompletionHandler<Integer,? super A> handler)
// 其余方法忽略,基本大同小异
...

除了这种通过 CompletionHandler 接口实现的回调,还可以通过 Future 类来实现。

Future 类:

当我们调用 AIO 套接字的 accept() 方法或 write() 等方法时,默认返回 Future 对象,具体源码如下:

public abstract Future<AsynchronousSocketChannel> accept();
public abstract Future<Integer> write(ByteBuffer src);

Future 类主要用来表示可能还没有完成的异步返回结果,其中它的常用方法如下:

  • cancel() :停止某个任务
  • isCancelled():判断任务是否停止
  • isDone():判断任务是否结束
  • get():当任务结束时返回结果,如果任务没结束,将一直阻塞
  • get(long timeout, TimeUnit unit):最多等待 timeout 个单位时间,否则返回 null

和 CompletionHandler 接口相比,Future 类还需要开发者手动判断任务是否执行结束,而 CompletionHandler 直接在任务完成时回调。但 Future 可以随时判断任务状态,在任务未完成前可以先执行其他操作,资源利用率更高,两者各有利弊。


AIO 和 NIO

和 NIO 的 SocketChannel、ServerSocketChannel 不同,AIO 采用全新的 AsynchronousSocketChannel 以及 AsynchronousServerSocketChannel,这里 Asynchronous 就表示异步的意思,连起来就是指:异步的 NIO。

相比 NIO 的所有 Channel 注册在 selector 轮询,AIO 直接采用异步回调的方式完成,这种处理方式相比原始 NIO 更优雅以及简单。在 NIO 中我们需要维护 selector 线程轮询所有 Socket,而 AIO 无须维护任何线程,只要保证主线程不停,异步调用后静静等待回调即可,更容易理解和开发。

关于 AIO 的介绍就写到这里。


各种 IO 模型对比

最后我通过表格的形式给出各种 IO 模型的对比:

同步阻塞 IO(BIO)伪异步 IO非阻塞 IO(NIO)异步 IO(AIO)
客户端个数 : IO 线程数1 : 1M : N(M 可以大于 N)M : 1(一个 selector 管理多个客户端连接)M : 0(异步回调,无须管理线程)
IO 类型(阻塞)阻塞 IO阻塞 IO非阻塞 IO非阻塞 IO
IO 类型(同步)同步同步同步异步
可靠性非常差
吞吐量
使用难度简单简单非常复杂复杂

伪异步 IO:伪异步 IO 是指服务端收到客户端请求后,将该请求封装成一个任务放入线程池。此时客户端请求可以直接返回,不阻塞,服务端轮询线程池中客户端发来的请求,请求处理完之后通知客户端。


参考
《Netty 权威指南》
https://www.apiref.com/java11-zh/java.base/java/nio/channels/CompletionHandler.html
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值