(七)异步编程AIO
1.从操作系统的内核角度对比三大模型
1.1 阻塞式I/O---对应BIO
1.2 非阻塞式I/O----对应NIO的非阻塞模式
注意:上图并不包括NIO的Selector监听模式,仅仅对应NIO中把服务端ServerSocketChannel和客户端SocketChannel配置的非阻塞模式。
1.2.1 I/O多路复用----对于NIO + Selector
- 第一次系统调用,如果数据尚未准备好,就要求内核监听I/O通道(监听的过程类似于select()函数,是阻塞式的)。直到有数据可供应用程序操作,再通知应用程序。
- 多路是指,系统内核能够监听多个IO通道。
-
(六)NIO聊天室实战——如同之前的案例一样,我们不仅使用了非阻塞式模型,还使用到了I/O多路复用。
- Selector还可以翻译成多路复用器。
1.3 异步I/O----AIO模型
- 如上三种模型本质上都是同步模型, 同步指的是,如果数据没准备好,即使没有被阻塞住,但想要得到要等待的数据,必须要再发起一次新的系统调用。
- 下图的异步体现在:我们程序只对数据发起了一次请求,没有请求到,就直接返回了,而之后,当这个数据已经准备好的时候,系统回来通知我们,而不需要我们再次发起请求,就能获取到这个数据,这就体现了异步的特点。A就是asynchronous,也就是异步的意思
2. 异步调用机制
如何实现异步操作的呢?有两种方法。
(1)通过返回Future对象
注意Future对象的get()方法是阻塞式的,直到Future所对应的任务返回了。
(2)CompletionHandler(常用)
- 在执行调用时,传入CompletionHandler。
- CompletionHandler是一个接口,需要实现其两个方法(回调函数),1.当I/O操作完成的话,需要执行什么逻辑;2.当I/O操作失败,需要执行什么逻辑。
3.“回音壁”实验结果
4.完整代码
4.1服务器端
public class Server { final String LOCAL_HOST = "localhost"; final int DEFAULT_PORT = 8888; AsynchronousServerSocketChannel serverChannel; private void close(Closeable closeable) { if (closeable != null) { try { closeable.close(); System.out.println("关闭" + closeable); } catch (IOException e) { e.printStackTrace(); } } } public void start() { try { // 绑定监听端口 // serverChannel底层默认绑定了一个AsynchronousChannelGroup // AsynchronousChannelGroup所包含的线程池中的线程用于各种异步回调函数的操作 serverChannel = AsynchronousServerSocketChannel.open(); serverChannel.bind(new InetSocketAddress(LOCAL_HOST,DEFAULT_PORT)); System.out.println("启动服务器,监听端口:" + DEFAULT_PORT); // accept()是异步的调用 while (true) { serverChannel.accept(null, new AcceptHandler());// 参数attachment类似于邮件的附件,也可以不放 // 小技巧:阻塞住,保证服务器的主线程不过早的返回,同时避免过于频繁的调用accept函数 System.in.read(); // read()是阻塞式调用 } } catch (IOException e) { e.printStackTrace(); } finally { close(serverChannel); } } // 用于处理serverChannel异步调用accept()函数的结果 private class AcceptHandler implements CompletionHandler<AsynchronousSocketChannel, Object> { @Override public void completed(AsynchronousSocketChannel result, Object attachment) { if (serverChannel.isOpen()) { // 确保服务端还在运行,让服务器继续等待下一个客户端的连接请求 // 底层已经进行了保护,不用担心出现栈溢出的问题 serverChannel.accept(null, this); } // 处理已连接客户端的读写操作, 读写仍然是异步的 AsynchronousSocketChannel clientChannel = result; if (clientChannel != null && clientChannel.isOpen()) { // 处理客户端通道上异步调用的读/写操作 ClientHandler handler = new ClientHandler(clientChannel); ByteBuffer buffer = ByteBuffer.allocate(1024); // attachmentInfo包含了clientChannel的回调函数在处理read()结果时需要用到的信息 Map<String, Object> attachmentInfo = new HashMap<>(); attachmentInfo.put("type", "read"); attachmentInfo.put("buffer", buffer); clientChannel.read(buffer, attachmentInfo, handler); // 接收客户端发来的消息,写入到buffer } } @Override public void failed(Throwable exc, Object attachment) { // 处理错误 } } // 处理异步调用clientChannel的read和write操作结束后返回的结果 private class ClientHandler implements CompletionHandler<Integer, Object>{ private AsynchronousSocketChannel clientChannel; public ClientHandler(AsynchronousSocketChannel channel) { this.clientChannel = channel; } @Override public void completed(Integer result, Object attachment) { Map<String, Object> info = (Map<String, Object>) attachment; // 判断完结的操作是读操作 还是 写操作呢? String type = (String) info.get("type"); // 如果是读操作完成了,拿到读进buffer的数据,把它写回ClientChannel(即让ClientChannel再读取buffer) if ("read".equals(type)) { ByteBuffer buffer = (ByteBuffer) info.get("buffer"); buffer.flip(); // 写模式变为读模式 // 更改attachment的type为写 info.put("type", "write"); clientChannel.write(buffer, info, this); // 由buffer读出数据,写入clientChannel } else if ("write".equals(type)) { // 把信息原封不动发回客户后,继续监听客户发来的消息 ByteBuffer buffer = ByteBuffer.allocate(1024); info.put("type", "read"); // 更新为read info.put("buffer", buffer); clientChannel.read(buffer, info, this); } } @Override public void failed(Throwable exc, Object attachment) { // 处理错误 } } public static void main(String[] args) { Server server = new Server(); server.start(); } }
4.2客户端
public class Client { final String LOCAL_HOST = "localhost"; final int DEFAULT_PORT = 8888; AsynchronousSocketChannel clientChannel; private void close(Closeable closeable) { if (closeable != null) { try { closeable.close(); System.out.println("关闭" + closeable); } catch (IOException e) { e.printStackTrace(); } } } public void start() { try { // 创建channel clientChannel = AsynchronousSocketChannel.open(); // 与服务器不同,演示异步调用机制的另一种实现方法:通过返回Future对象来完成异步机制 // connect是异步调用 Future<Void> future = clientChannel.connect(new InetSocketAddress(LOCAL_HOST,DEFAULT_PORT)); future.get(); // 阻塞式调用,直到建立连接再进行下一步操作 // 等待用户的输入 BufferedReader consoleReader = new BufferedReader(new InputStreamReader(System.in)); while (true) { String input = consoleReader.readLine(); // 阻塞式调用 // ① 发送给服务端 byte[] inputBytes = input.getBytes(); ByteBuffer buffer = ByteBuffer.wrap(inputBytes); // 把用户输入的数据放入到buffer Future<Integer> writeResult = clientChannel.write(buffer); // 把buffer的数据写入到客户端的channel,注意write也是异步的,返回值表示写了多少字节 writeResult.get(); // 阻塞,等待把buffer的数据写入到客户端的channel,发送给服务器 // ②读取从服务端发回的消息 buffer.flip(); // 读模式变为写模式 Future<Integer> readResult = clientChannel.read(buffer); // 异步 readResult.get(); // 阻塞式等待read操作完成 String echo = new String(buffer.array()); buffer.clear(); System.out.println("Server : " + echo); } } catch (IOException e) { e.printStackTrace(); } catch (InterruptedException e) { e.printStackTrace(); } catch (ExecutionException e) { e.printStackTrace(); } finally { close(clientChannel); } } public static void main(String[] args) { Client client = new Client(); client.start(); } }