一、简介
AIO 是彻底的异步通信。
【有一个经典的举例】:烧开水;假设有这么一个场景,有一排水壶(客户)在烧水。
- AIO 的做法是,每个水壶上装一个开关,当水开了以后会提醒对应的线程去处理。
- NIO 的做法是,叫一个线程不停的循环观察每一个水壶,根据每个水壶当前的状态去处理。
- BIO 的做法是,叫一个线程停留在一个水壶那,直到这个水壶烧开,才去处理下一个水壶。
可以看出AIO是最聪明省力,NIO相对省力,叫一个人就能看所有的壶,BIO最效率最低。
异步I/O(Asynchronous I/O,简称AIO)是Java中的一种高级I/O模型,它允许线程发起I/O请求后立即返回,而不必等待操作完成。与非阻塞NIO不同的是,AIO的操作是完全异步的,即I/O操作会在后台执行,并通过回调或轮询机制通知结果。
当进行读写操作时,只须直接调用API的 read/write 方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入 read() 方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将 write() 方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在jdk1.7版本开始支持AIO,这部分内容被称作NIO2,AIO模型需要操作系统的支持。
- 在 Windows 操作系统中,提供了一个叫做 I/O Completion Ports 的方案,通常简称为 IOCP,操作系统负责管理线程池,其性能非常优异,所以在 Windows 中 JDK 直接采用了 IOCP 的支持。
- 而在 Linux 中其实也是有AIO 的实现的,但是限制比较多,性能也一般,所以 JDK 采用了自建线程池的方式,也就是说JDK并没有用Linux提供的AIO。
- libeio 的 AIO
- glibc 的 AIO
主要在 Java.nio.channels 包下增加了下面四个异步通道:
- AsynchronousSocketChannel :客户端Socket通道类,负责客户端消息读写
- AsynchronousServerSocketChannel :服务端Socket通道类,负责服务端Socket的创建和监听
- AsynchronousFileChannel
- AsynchronousDatagramChannel

二、AIO主要API
- AsynchronousServerSocketChannel :服务端Socket通道类,负责监听客户端连接请求;
- AsynchronousSocketChannel :客户端Socket通道类,负责客户端消息读写;
- CompletionHandler :消息处理回调接口,是一个负责消费异步IO操作结果的消息处理器;
- ByteBuffer :负责承载通信过程中需要读、写的消息。
- AsynchronousChannelGroup :用于异步通道资源共享。
2.1 AsynchronousServerSocketChannel
AsynchronousServerSocketChannel 是一个流式监听套接字的异步通道。
其使用需要经过三个步骤:创建/打开通道、绑定地址和端口、监听客户端连接请求。
(1)创建/打开通道
通过调用 AsynchronousServerSocketChannel#open() 来创建 AsynchronousServerSocketChannel 实例:
try {
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
} catch (IOException e) {
e.printStackTrace();
}
当打开通道失败时,会抛出一个 IOException 异常。AsynchronousServerSocketChannel 提供了设置通道分组(AsynchronousChannelGroup)的功能,以实现组内通道资源共享。可以调用 AsynchronousServerSocketChannel#open(AsynchronousChannelGroup) 重载方法创建指定分组的通道:
try {
ExecutorService pool = Executors.newCachedThreadPool();
AsynchronousChannelGroup group = AsynchronousChannelGroup.withCachedThreadPool(pool, 10);
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open(group);
} catch (IOException e) {
e.printStackTrace();
}
AsynchronousChannelGroup 封装了处理由绑定到组的异步通道所触发的I/O操作完成所需的机制。每个 AsynchronousChannelGroup 关联了一个被用于提交处理I/O事件和分发消费在组内通道上执行的异步操作结果的 completion-handlers 的线程池。除了处理I/O事件,该线程池还有可能处理其他一些用于支持完成异步I/O操作的任务。从上面例子可以看到,通过指定 AsynchronousChannelGroup 的方式打开 AsynchronousServerSocketChannel ,可以定制 server channel 执行的线程池。有关AsynchronousChannelGroup的详细介绍可以查看官方文档注释。如果不指定AsynchronousChannelGroup,则AsynchronousServerSocketChannel会归类到一个默认的分组中。
(2)绑定地址和端口
通过调用 AsynchronousServerSocketChannel#bind(SocketAddress) 方法来绑定监听地址和端口:
// 构建一个InetSocketAddress实例以指定监听的地址和端口,如果需要指定ip,则调用InetSocketAddress(ip,port)构造方法创建即可
serverSocketChannel.bind(new InetSocketAddress(port));
(3)监听和接收客户端连接请求
监听客户端连接请求,主要通过调用 AsynchronousServerSocketChannel#accept() 方法完成。
accept() 有两个重载方法:
- public abstract void accept(A, CompletionHandler);
- public abstract Future accept();
这两个重载方法的行为方式完全相同,事实上,AIO的很多异步API都封装了诸如此类的重载方法:提供 CompletionHandle 回调参数或者返回一个 Future 类型变量。
用过 Future 接口的都知道,可以调用 Future.get() 方法阻塞等待调用结果。以第一个重载方法为例,当接受一个新的客户端连接,或者 accept() 操作发生异常时,会通过 CompletionHandler 将结果返回给用户处理:
AsynchronousServerSocketChannel serverSocketChannel = AsynchronousServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8888));
serverSocketChannel.accept(serverSocketChannel, new CompletionHandler<AsynchronousSocketChannel,
AsynchronousServerSocketChannel>() {
// 接收到新的客户端连接时调用,result就是和客户端的连接对话,此时可以通过result和客户端进行通信
@Override
public void completed(final AsynchronousSocketChannel result,
final AsynchronousServerSocketChannel attachment) {
// 接收到新的客户端连接时回调
// result即和该客户端的连接会话
// 此时可以通过result与客户端进行交互
}
@Override
public void failed(final Throwable exc, final AsynchronousServerSocketChannel attachment) {
// accept失败时回调
}
});
需要注意的是,AsynchronousServerSocketChannel 是线程安全的,但在任何时候同一时间内只能允许有一个 accept() 操作。
因此,必须得等待前一个accept() 操作完成之后才能启动下一个accept() :
serverSocketChannel.accept(serverSocketChannel, new CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel>() {
@Override
public void completed(final AsynchronousSocketChannel result,
final AsynchronousServerSocketChannel attachment) {
// 接收到新的客户端连接,此时本次accept已经完成
// 继续监听下一个客户端连接到来
serverSocketChannel.accept(serverSocketChannel,this);
// result即和该客户端的连接会话
// 此时可以通过result与客户端进行交互
}
@Override
public void failed(Throwable exc, AsynchronousServerSocketChannel attachment) {
// accept失败时回调
}
});
此外,还可以通过以下方法获取和设置 AsynchronousServerSocketChannel 的 socket 选项:
// 设置socket选项,StandardSocketOptions类封装了常用的socket设置选项
serverSocketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE, true);
// 获取socket选项设置
boolean keepAlive = serverSocketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
获取本地地址:
InetSocketAddress address = (InetSocketAddress) serverSocketChannel.getLocalAddress();
2.2 AsynchronousSocketChannel
AsynchronousSocketChannel 是一个流式连接套接字的异步通道。其表示服务端与客户端之间的连接通道。客户端可以通过调用 AsynchronousSocketChannel#open() 创建,而服务端则通过调用 AsynchronousServerSocketChannel#accept() 方法后由 AIO 内部在合适的时候创建。下面以客户端实现为例,介绍AsynchronousSocketChannel:
(1)创建 AsynchronousSocketChannel 并连接到服务端
需要通过 AsynchronousSocketChannel#open() 创建和打开一个 AsynchronousSocketChannel 实例,再调用其 connect() 方法连接到服务端,接着才可以与服务端交互:
// 打开一个socket通道
AsynchronousSocketChannel socketChannel = AsynchronousSocketChannel.open();
// 阻塞等待连接成功
socketChannel.connect(new InetSocketAddress(ip,port)).get();
// 连接成功,接下来可以进行read、write操作
同 AsynchronousServerSocketChannel 一样,AsynchronousSocketChannel 也提供了重载方法 open(AsynchronousChannelGroup) 用于指定通道分组和定制线程池。
连接到服务端由 socketChannel.connect() 实现,connect() 包含了两个重载方法:
- public final Future connect(SocketAddress var1);
- public final void connect(SocketAddress var1, A var2, CompletionHandler var3)
上面例子使用带Future返回值的重载,并调用 Future#get() 方法阻塞等待连接建立完成。
(2)发送消息
可以构建一个 ByteBuffer 对象并调用 socketChannel.write(ByteBuffer) 方法异步发送消息,并通过 CompletionHandler 回调接收处理发送结果:
ByteBuffer writeBuf = ByteBuffer.wrap("From socketChannel:Hello i am socketChannel".getBytes());
socketChannel.write(writeBuf, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(final Integer result, final Object attachment) {
// 发送完成,result:总共写入的字节数
}
@Override
public void failed(final Throwable exc, final Object attachment) {
// 发送失败
}
});
(3)读取消息
构建一个指定接收长度的 ByteBuffer 用于接收数据,调用 socketChannel.read() 方法读取消息并通过 CompletionHandler 处理读取结果:
ByteBuffer readBuffer = ByteBuffer.allocate(128);
socketChannel.read(readBuffer, null, new CompletionHandler<Integer, Object>() {
@Override
public void completed(final Integer result, final Object attachment) {
// 读取完成,result:实际读取的字节数。如果通道中没有数据可读则result=-1。
}
@Override
public void failed(final Throwable exc, final Object attachment) {
// 读取失败
}
});
此外,AsynchronousSocketChannel 也封装了设置/获取socket选项的方法:
// 设置socket选项
socketChannel.setOption(StandardSocketOptions.SO_KEEPALIVE,true);
// 获取socket选项设置
boolean keepAlive = socketChannel.getOption(StandardSocketOptions.SO_KEEPALIVE);
2.3 CompletionHandler
CompletionHandler 是一个用于消费异步I/O操作结果的处理器。
AIO中定义的异步通道允许指定一个 CompletionHandler 处理器消费一个异步操作的结果。从上文中也可以看到,AIO中大部分的异步I/O操作接口都封装了一个带 CompletionHandler 类型参数的重载方法,使用 CompletionHandler 可以很方便地处理AIO中的异步I/O操作结果。CompletionHandler 是一个具有两个泛型类型参数的接口,声明了两个接口方法:
public interface CompletionHandler<V,A> {
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
}
- 泛型V :表示I/O操作的结果类型,通过该类型参数消费I/O操作的结果;
- 泛型A :为附加到I/O操作中的对象类型,可以通过该类型参数将需要的变量传入到 CompletionHandler 实现中使用。
因此,AIO中大部分的异步I/O操作都有一个类似这样带 CompletionHandler 类型参数的重载方法:
<V,A> void ioOperate(params, A attachment, CompletionHandler<V,A> handler);
例如,AsynchronousServerSocketChannel#accept()方法:
public abstract <A> void accept(A attachment, CompletionHandler<AsynchronousSocketChannel,? super A> handler);
例如,AsynchronousSocketChannel#write()方法等:
public final <A> void write(ByteBuffer src,A attachment,CompletionHandler<Integer,? super A> handler)
当I/O操作成功完成时,会回调到 completed() 方法,failed() 方法则在I/O操作失败时被回调。
需要注意的是:在 CompletionHandler 的实现中应当及时处理操作结果,以避免一直占用调用线程而不能分发其他的 CompletionHandler 处理器。
三、代码实例
CompletionHandler 处理器:
/**
* @description: client连接后的回调函数
* @author: wang
* @date: 2022/12/5 15:20
*/
public class AioHandler implements CompletionHandler<AsynchronousSocketChannel, AioServer> {
@Override
public void completed(AsynchronousSocketChannel result, AioServer attachment) {
// 处理下一次的client连接。类似链式调用
attachment.getServerChannel().accept(attachment, this);
// 执行业务逻辑
doRead(result);
}
/**
* 读取client发送的消息打印到控制台
* AIO中OS已经帮助我们完成了read的IO操作,所以我们直接拿到了读取的结果
* @param clientChannel 服务端于客户端通信的 channel
*/
private void doRead(AsynchronousSocketChannel clientChannel) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
// 从client读取数据,在我们调用clientChannel.read()之前OS,已经帮我们完成了IO操作
// 我们只需要用一个缓冲区来存放读取的内容即可
clientChannel.read(
buffer, // 用于数据中转缓冲区
buffer, // 用于存储client发送的数据的缓冲区
new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer attachment) {
System.out.println("receive client data length:" + attachment.capacity() + " byte");
attachment.flip(); // 移动 limit位置
// 读取client发送的数据
System.out.println("from client : "+new String(attachment.array(), StandardCharsets.UTF_8));
// 向client写入数据
doWrite(clientChannel);
}
@Override
public void failed(Throwable exc, ByteBuffer attachment) {
}
}
);
}
private void doWrite(AsynchronousSocketChannel clientChannel) {
// 向client发送数据,clientChannel.write()是一个异步调用,该方法执行后会通知
// OS执行写的IO操作,会立即返回
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner s = new Scanner(System.in);
String line = s.nextLine();
buffer.put(line.getBytes(StandardCharsets.UTF_8));
buffer.flip();
clientChannel.write(buffer);
// clientChannel.write(buffer).get(); // 会进行阻塞,直到OS写操作完成
}
/**
* 异常处理逻辑
*
* @param exc
* @param attachment
*/
@Override
public void failed(Throwable exc, AioServer attachment) {
exc.printStackTrace();
}
}
服务端:
/**
* @description: 服务端
* @author: wang
* @date: 2022/12/5 15:20
*/
public class AioServer {
private ExecutorService service;
private AsynchronousServerSocketChannel serverChannel;
public ExecutorService getService() {
return service;
}
public AsynchronousServerSocketChannel getServerChannel() {
return serverChannel;
}
public AioServer(int port) {
init(port);
}
private void init(int port) {
System.out.println("server starting at port "+port+"..");
// 初始化定长线程池
service = Executors.newFixedThreadPool(4);
try {
// 初始化 AsyncronousServersocketChannel
serverChannel = AsynchronousServerSocketChannel.open();
// 监听端口
serverChannel.bind(new InetSocketAddress(port));
// 监听客户端连接,但在AIO,每次accept只能接收一个client,所以需要
// 在处理逻辑种再次调用accept用于开启下一次的监听
// 类似于链式调用
serverChannel.accept(this, new AioHandler());
try {
// 阻塞程序,防止被GC回收
TimeUnit.SECONDS.sleep(Long.MAX_VALUE);
} catch (InterruptedException e) {
e.printStackTrace();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
new AioServer(8000);
}
}
客户端:
/**
* @description: 客户端
* @author: wang
* @date: 2022/12/5 15:20
*/
public class AioClient {
private AsynchronousSocketChannel clientChannel;
public AioClient(String host, int port) {
init(host,port);
}
private void init(String host, int port) {
try {
clientChannel = AsynchronousSocketChannel.open();
clientChannel.connect(new InetSocketAddress(host,port));
} catch (IOException e) {
e.printStackTrace();
}
}
public void doWrite(String line) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put(line.getBytes(StandardCharsets.UTF_8));
buffer.flip();
clientChannel.write(buffer);
}
public void doRead() {
ByteBuffer buffer = ByteBuffer.allocate(1024);
try {
// read() 是一个异步方法,实际由OS实现,
// get()会阻塞,此处使用阻塞是因为后面要把结果打印
// 也可以去掉get,但是就必须实现 CompletionHandler
// 就像server端读取数据那样
clientChannel.read(buffer).get();
buffer.flip();
System.out.println("from server: "+new String(buffer.array(),StandardCharsets.UTF_8));
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
public void doDestory() {
if (null != clientChannel) {
try {
clientChannel.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
AioClient client = new AioClient("localhost", 8000);
try {
System.out.println("enter your message to server : ");
Scanner s = new Scanner(System.in);
String line = s.nextLine();
client.doWrite(line);
client.doRead();
} finally {
client.doDestory();
}
}
}

被折叠的 条评论
为什么被折叠?



