概述
上篇博客我简单介绍了 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 : 1 | M : N(M 可以大于 N) | M : 1(一个 selector 管理多个客户端连接) | M : 0(异步回调,无须管理线程) |
IO 类型(阻塞) | 阻塞 IO | 阻塞 IO | 非阻塞 IO | 非阻塞 IO |
IO 类型(同步) | 同步 | 同步 | 同步 | 异步 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |
使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
伪异步 IO:伪异步 IO 是指服务端收到客户端请求后,将该请求封装成一个任务放入线程池。此时客户端请求可以直接返回,不阻塞,服务端轮询线程池中客户端发来的请求,请求处理完之后通知客户端。