#源码解析AIO,如果不了解,不妨来看看!(非常详细)从零基础到精通,收藏这篇就够了!

什么是AIO?

Java 7的NIO提供了异步Channel支持,这种异步Channel可以提供更高效的IO,这种基于异步Channel的IO机制也被称为异步IO(Asynchronous IO) 前面介绍的NIO其实是基于Channel的非阻塞IO(即同步IO),接下来则是针对异步IO进行介绍。

定义:AIO是指当应用程序发起IO操作时,无需等待操作完成即可继续执行其他任务。【核心是“异步”和“非阻塞”】

实现原理:AIO基于事件和回调机制,应用程序向操作系统注册IO监听,然后继续做自己的事情,操作系统在IO操作完成后主动通知应用程序,并通过回调函数处理结果。

image.png

image.png

AIO与NIO的区别:

NIO基于多路复用器实现同步非阻塞,需轮询检查IO状态

AIO基于操作系统完成IO操作后通知应用,无需主动轮询

和同步IO一样,异步IO也是由操作系统进行支持的。微软的windows系统提供了一种异步IO技术: IOCP(I/O Completion Port,I/O完成端口);

Linux下由于没有这种异步IO技术,所以使用的是epoll(上文介绍过的一种多路复用IO技术的实现)对异步IO进行模拟。

JAVA对AIO的支持

JAVA AIO框架简析

NIO提供了一系列Asynchronous开头的Channel接口和类

image.png

image.png

  • AsynchronousSocketChannel:客户端异步socket

    open():打开异步socket客户端通道

    bind():将通道Socket绑定到本地地址,并配置监听连接

    setOption():设置Socket参数

    shutdownInput():在不关闭通道的情况下关闭读取连接

    shutdownOutput():在不关闭通道的情况下关闭写入连接

    getRemoteAddress():返回此通道Socket连接到的远程地址

    connect():连接指定地址的通道

    read():读数据

    write():写数据

    getLocalAddress():返回此通道的Socket绑定的套接字地址

    public class AioClient { public static void main(String[] args) throws Exception { AsynchronousSocketChannel channel = AsynchronousSocketChannel.open(); channel.connect(new InetSocketAddress("127.0.0.1", 8080)); ByteBuffer buffer = ByteBuffer.allocate(1024); buffer.put("Java AIO".getBytes(StandardCharsets.UTF_8)); buffer.flip(); Thread.sleep(1000L); channel.write(buffer); } }

  • AsynchronousServerSocketChannel:服务端异步socket

    open():打开异步socket服务端通道

    bind():将通道Socket绑定到本地地址,并配置监听连接

    setOption():设置Socket参数

    accept():接受客户端建立连接,返回值是void,表示回调触发结果处理;返回值是Future表示待定结果

    演示示例:

    public class AioServer { public static void main(String[] args) throws IOException { System.out.println(Thread.currentThread().getName() + " AioServer start"); AsynchronousServerSocketChannel serverChannel = AsynchronousServerSocketChannel.open() .bind(new InetSocketAddress("127.0.0.1", 8080)); serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Void>() { @Override public void completed(AsynchronousSocketChannel clientChannel, Void attachment) { System.out.println(Thread.currentThread().getName() + " client is connected"); ByteBuffer buffer = ByteBuffer.allocate(1024); clientChannel.read(buffer, buffer, new ClientHandler()); } @Override public void failed(Throwable exc, Void attachment) { System.out.println("accept fail"); } }); System.in.read(); } }

  • AsynchronousFileChannel:文件操作异步读写

    open():打开异步socket通道

    size():返回该文件当前大小

    truncate():截断当前通道文件为给定大小

    force():强制将此通道文件的任何更新写入包含它的存储设备

    lock():获取此通道文件给定区域的锁。

    tryLock():尝试获取此通道文件的给定区域的锁

    read():读文件数据

    write():写文件数据

    Path path = Paths.get("/data/code/github/java_practice/src/main/resources/1log4j.properties"); AsynchronousFileChannel channel = AsynchronousFileChannel.open(path); ByteBuffer buffer = ByteBuffer.allocate(1024); Future<Integer> future = channel.read(buffer,0); // while (!future.isDone()){ // System.out.println("I'm idle"); // } Integer readNumber = future.get(); buffer.flip(); CharBuffer charBuffer = CharBuffer.allocate(1024); CharsetDecoder decoder = Charset.defaultCharset().newDecoder(); decoder.decode(buffer,charBuffer,false); charBuffer.flip(); String data = new String(charBuffer.array(),0, charBuffer.limit()); System.out.println("read number:" + readNumber); System.out.println(data);

  • **CompletionHandler<V,A>**:回调接口,需实现completed()和failed()方法

    completed():完成时回调方法

    failed():失败时回调方法

    public class ClientHandler implements CompletionHandler<Integer, ByteBuffer> { @Override public void completed(Integer result, ByteBuffer buffer) { buffer.flip(); byte [] data = new byte[buffer.remaining()]; buffer.get(data); System.out.println(Thread.currentThread().getName() + " received:" + new String(data, StandardCharsets.UTF_8)); } @Override public void failed(Throwable exc, ByteBuffer buffer) { } }

Java AIO编程的两种方式

通过AIO常用类与方法分析,我们发现accept()方法的两种返回值形式,一个是Future,一个是void,这就引出了AIO编程的两种方式

  1. 使用Future类

    // 定义文件路径 Path path = Paths.get("example.txt"); try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(path, StandardOpenOption.READ)) { // 创建一个 ByteBuffer 用于存储读取的数据 ByteBuffer buffer = ByteBuffer.allocate(1024); // 启动异步读取操作,返回一个 Future 对象 Future<Integer> result = fileChannel.read(buffer, 0); // 可以在此处执行其他任务,因为读取操作是异步的 // 等待异步操作完成并获取读取的字节数 while (!result.isDone()) { System.out.println(" 异步读取操作正在进行中..."); Thread.sleep(100); } int bytesRead = result.get(); System.out.println(" 读取的字节数: " + bytesRead); // 处理读取的数据 buffer.flip(); byte[] data = new byte[buffer.remaining()]; buffer.get(data); String content = new String(data); System.out.println(" 读取的内容: " + content); } catch (IOException | InterruptedException | java.util.concurrent.ExecutionException e) { e.printStackTrace(); }

  2. 使用CompletionHandler接口

    channel.read(buffer, null, new CompletionHandler<Integer, Void>() { @Override public void completed(Integer bytesRead, Void attachment) { // 处理读取的数据 System.out.println("Read " + bytesRead + " bytes"); } @Override public void failed(Throwable exc, Void attachment) { exc.printStackTrace(); channel.close(); } });

代码实例

下面,通过一个代码示例,演示JAVA AIO框架的具体使用,先上代码,在针对代码编写和运行中的要点进行讲解:

package testASocket; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.AsynchronousChannelGroup; import java.nio.channels.AsynchronousServerSocketChannel; import java.nio.channels.AsynchronousSocketChannel; import java.nio.channels.CompletionHandler; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.apache.log4j.BasicConfigurator; /** * @author yinwenjie */ public class SocketServer { static { BasicConfigurator.configure(); } private static final Object waitObject = new Object(); /** * @param args * @throws Exception */ public static void main(String[] args) throws Exception { /* * 对于使用的线程池技术,我一定要多说几句 * 1、Executors是线程池生成工具,通过这个工具我们可以很轻松的生成“固定大小的线程池”、“调度池”、“可伸缩线程数量的池”。具体请看API Doc * 2、当然也可以通过ThreadPoolExecutor直接生成池。 * 3、这个线程池是用来得到操作系统的“IO事件通知”的,不是用来进行“得到IO数据后的业务处理的”。要进行后者的操作,您可以再使用一个池(最好不要混用) * 4、您也可以不使用线程池(不推荐),如果决定不使用线程池,直接AsynchronousServerSocketChannel.open()就行了。 * */ ExecutorService threadPool = Executors.newFixedThreadPool(20); AsynchronousChannelGroup group = AsynchronousChannelGroup.withThreadPool(threadPool); final AsynchronousServerSocketChannel serverSocket = AsynchronousServerSocketChannel.open(group); //设置要监听的端口“0.0.0.0”代表本机所有IP设备 serverSocket.bind(new InetSocketAddress("0.0.0.0", 83)); //为AsynchronousServerSocketChannel注册监听,注意只是为AsynchronousServerSocketChannel通道注册监听 //并不包括为 随后客户端和服务器 socketchannel通道注册的监听 serverSocket.accept(null, new ServerSocketChannelHandle(serverSocket)); //等待,以便观察现象(这个和要讲解的原理本身没有任何关系,只是为了保证守护线程不会退出) synchronized(waitObject) { waitObject.wait(); } } } /** * 这个处理器类,专门用来响应 ServerSocketChannel 的事件。 * @author yinwenjie */ class ServerSocketChannelHandle implements CompletionHandler<AsynchronousSocketChannel, Void> { /** * 日志 */ private static final Log LOGGER = LogFactory.getLog(ServerSocketChannelHandle.class); private AsynchronousServerSocketChannel serverSocketChannel; /** * @param serverSocketChannel */ public ServerSocketChannelHandle(AsynchronousServerSocketChannel serverSocketChannel) { this.serverSocketChannel = serverSocketChannel; } /** * 注意,我们分别观察 this、socketChannel、attachment三个对象的id。 * 来观察不同客户端连接到达时,这三个对象的变化,以说明ServerSocketChannelHandle的监听模式 */ @Override public void completed(AsynchronousSocketChannel socketChannel, Void attachment) { ServerSocketChannelHandle.LOGGER.info("completed(AsynchronousSocketChannel result, ByteBuffer attachment)"); //每次都要重新注册监听(一次注册,一次响应),但是由于“文件状态标示符”是独享的,所以不需要担心有“漏掉的”事件 this.serverSocketChannel.accept(attachment, this); //为这个新的socketChannel注册“read”事件,以便操作系统在收到数据并准备好后,主动通知应用程序 //在这里,由于我们要将这个客户端多次传输的数据累加起来一起处理,所以我们将一个stringbuffer对象作为一个“附件”依附在这个channel上 // ByteBuffer readBuffer = ByteBuffer.allocate(50); socketChannel.read(readBuffer, new StringBuffer(), new SocketChannelReadHandle(socketChannel , readBuffer)); } /* (non-Javadoc) * @see java.nio.channels.CompletionHandler#failed(java.lang.Throwable, java.lang.Object) */ @Override public void failed(Throwable exc, Void attachment) { ServerSocketChannelHandle.LOGGER.info("failed(Throwable exc, ByteBuffer attachment)"); } } /** * 负责对每一个socketChannel的数据获取事件进行监听。<p> * * 重要的说明: 一个socketchannel都会有一个独立工作的SocketChannelReadHandle对象(CompletionHandler接口的实现), * 其中又都将独享一个“文件状态标示”对象FileDescriptor、 * 一个独立的由程序员定义的Buffer缓存(这里我们使用的是ByteBuffer)、 * 所以不用担心在服务器端会出现“窜对象”这种情况,因为JAVA AIO框架已经帮您组织好了。<p> * * 但是最重要的,用于生成channel的对象: AsynchronousChannelProvider是单例模式,无论在哪组socketchannel, * 对是一个对象引用(但这没关系,因为您不会直接操作这个AsynchronousChannelProvider对象)。 * @author yinwenjie */ class SocketChannelReadHandle implements CompletionHandler<Integer, StringBuffer> { /** * 日志 */ private static final Log LOGGER = LogFactory.getLog(SocketChannelReadHandle.class); private AsynchronousSocketChannel socketChannel; /** * 专门用于进行这个通道数据缓存操作的ByteBuffer<br> * 当然,您也可以作为CompletionHandler的attachment形式传入。<br> * 这是,在这段示例代码中,attachment被我们用来记录所有传送过来的Stringbuffer了。 */ private ByteBuffer byteBuffer; public SocketChannelReadHandle(AsynchronousSocketChannel socketChannel , ByteBuffer byteBuffer) { this.socketChannel = socketChannel; this.byteBuffer = byteBuffer; } /* (non-Javadoc) * @see java.nio.channels.CompletionHandler#completed(java.lang.Object, java.lang.Object) */ @Override public void completed(Integer result, StringBuffer historyContext) { //如果条件成立,说明客户端主动终止了TCP套接字,这时服务端终止就可以了 if(result == -1) { try { this.socketChannel.close(); } catch (IOException e) { SocketChannelReadHandle.LOGGER.error(e); } return; } SocketChannelReadHandle.LOGGER.info("completed(Integer result, Void attachment) : 然后我们来取出通道中准备好的值"); /* * 实际上,由于我们从Integer result知道了本次channel从操作系统获取数据总长度 * 所以实际上,我们不需要切换成“读模式”的,但是为了保证编码的规范性,还是建议进行切换。 * * 另外,无论是JAVA AIO框架还是JAVA NIO框架,都会出现“buffer的总容量”小于“当前从操作系统获取到的总数据量”, * 但区别是,JAVA AIO框架中,我们不需要专门考虑处理这样的情况,因为JAVA AIO框架已经帮我们做了处理(做成了多次通知) * */ this.byteBuffer.flip(); byte[] contexts = new byte[1024]; this.byteBuffer.get(contexts, 0, result); this.byteBuffer.clear(); try { String nowContent = new String(contexts , 0 , result , "UTF-8"); historyContext.append(nowContent); SocketChannelReadHandle.LOGGER.info("================目前的传输结果: " + historyContext); } catch (UnsupportedEncodingException e) { SocketChannelReadHandle.LOGGER.error(e); } //如果条件成立,说明还没有接收到“结束标记” if(historyContext.indexOf("over") == -1) { return; } //========================================================================= // 和上篇文章的代码相同,我们以“over”符号作为客户端完整信息的标记 //========================================================================= SocketChannelReadHandle.LOGGER.info("=======收到完整信息,开始处理业务========="); historyContext = new StringBuffer(); //还要继续监听(一次监听一次通知) this.socketChannel.read(this.byteBuffer, historyContext, this); } /* (non-Javadoc) * @see java.nio.channels.CompletionHandler#failed(java.lang.Throwable, java.lang.Object) */ @Override public void failed(Throwable exc, StringBuffer historyContext) { SocketChannelReadHandle.LOGGER.info("=====发现客户端异常关闭,服务器将关闭TCP通道"); try { this.socketChannel.close(); } catch (IOException e) { SocketChannelReadHandle.LOGGER.error(e); } } }

要点讲解

  • 在JAVA NIO框架中,我们说到了一个重要概念“selector”(选择器)。它负责代替应用查询中所有已注册的通道到操作系统中进行IO事件轮询、管理当前注册的通道集合,定位发生事件的通道等操操作;但是在JAVA AIO框架中,由于应用程序不是“轮询”方式,而是订阅-通知方式,所以不再需要“selector”(选择器)了,改由channel通道直接到操作系统注册监听。

  • JAVA AIO框架中,只实现了两种网络IO通道“AsynchronousServerSocketChannel”(服务器监听通道)、“AsynchronousSocketChannel”(socket套接字通道)。但是无论哪种通道他们都有独立的fileDescriptor(文件标识符)、attachment(附件,附件可以使任意对象,类似“通道上下文”),并被独立的SocketChannelReadHandle类实例引用。我们通过debug操作来看看它们的引用结构:

在测试过程中,我们启动了两个客户端(客户端用什么语言来写都行,用阻塞或者非阻塞方式也都行,只要是支持 TCP Socket套接字的就行,然后我们观察服务器端对这两个客户端通道的处理情况:

image.png

image.png

可以看到,在服务器端分别为客户端1和客户端2创建的两个WindowsAsynchronousSocketChannelImpl对象为:

image.png

image.png

客户端1: WindowsAsynchronousSocketChannelImpl: 760 | FileDescriptor: 762

客户端2: WindowsAsynchronousSocketChannelImpl: 792 | FileDescriptor: 797

接下来,我们让两个客户端发送信息到服务器端,并观察服务器端的处理情况。客户端1发来的消息和客户端2发来的消息,在服务器端的处理情况如下图所示:

image.png

image.png

客户端1: WindowsAsynchronousSocketChannelImpl: 760 | FileDescriptor: 762 | SocketChannelReadHandle: 803 | HeapByteBuffer: 808

客户端2: WindowsAsynchronousSocketChannelImpl: 792 | FileDescriptor: 797 | SocketChannelReadHandle: 828 | HeapByteBuffer: 833

可以明显看到,服务器端处理每一个客户端通道所使用的SocketChannelReadHandle(处理器)对象都是独立的,并且所引用的SocketChannel对象都是独立的。

JAVA NIO和JAVA AIO框架,除了因为操作系统的实现不一样而去掉了Selector外,其他的重要概念都是存在的,例如上文中提到的Channel的概念,还有演示代码中使用的Buffer缓存方式。实际上JAVA NIO和JAVA AIO框架可以看成是一套完整的“高并发IO处理”的实现。

还有改进可能

当然,以上代码是示例代码,目标是为了让您了解JAVA AIO框架的基本使用。所以它还有很多改造的空间,例如:

在生产环境下,我们需要记录这个通道上“用户的登录信息”。那么这个需求可以使用JAVA AIO中的“附件”功能进行实现。

记住JAVA AIO 和 JAVA NIO 框架都是要使用线程池的(当然您也可以不用),线程池的使用原则,一定是只有业务处理部分才使用,使用后马上结束线程的执行(还回线程池或者消灭它)。JAVA AIO框架中还有一个线程池,是拿给“通知处理器”使用的,这是因为JAVA AIO框架是基于“订阅-通知”模型的,“订阅”操作可以由主线程完成,但是您总不能要求在应用程序中并发的“通知”操作也在主线程上完成吧_。

最好的改进方式,当然就是使用Netty或者Mina咯。

AIO的底层IO实现分析

image.png

image.png

JAVA AIO中类设计和操作系统是具有相关性的,JAVA AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO,这个从JAVA AIO框架的部分类设计上就可以看出来。例如框架中,在Windows下负责实现套接字通道的具体类是“sun.nio.ch.WindowsAsynchronousSocketChannelImpl”,其引用的IOCP类型文档注释如是:

/** * Windows implementation of AsynchronousChannelGroup encapsulating an I/O * completion port. */

完整代码建议从“java.nio.channels.spi.AsynchronousChannelProvider”这个类看起。

请注意图中的“java.nio.channels.NetworkChannel”接口,这个接口同样被JAVA NIO框架实现了,如下图所示:

image.png

image.png

为什么还有Netty

那么有的读者可能就会问,既然JAVA NIO / JAVA AIO已经实现了各主流操作系统的底层支持,那么为什么现在主流的JAVA NIO技术会是Netty和MINA呢? 答案很简单: 因为更好用,这里举几个方面的例子:

  • 虽然JAVA NIO 和 JAVA AIO框架提供了 多路复用IO/异步IO的支持,但是并没有提供上层“信息格式”的良好封装。例如前两者并没有提供针对 Protocol Buffer、JSON这些信息格式的封装,但是Netty框架提供了这些数据格式封装(基于责任链模式的编码和解码功能)

  • 要编写一个可靠的、易维护的、高性能的(注意它们的排序)NIO/AIO 服务器应用。除了框架本身要兼容实现各类操作系统的实现外。更重要的是它应该还要处理很多上层特有服务,例如: 客户端的权限、还有上面提到的信息格式封装、简单的数据读取。这些Netty框架都提供了响应的支持。

  • JAVA NIO框架存在一个poll/epoll bug: Selector doesn’t block on Selector.select(timeout),不能block意味着CPU的使用率会变成100%(这是底层JNI的问题,上层要处理这个异常实际上也好办)。当然这个bug只有在Linux内核上才能重现。

  • 这个问题在JDK 1.7版本中还没有被完全解决: http://bugs.java.com/bugdatabase/view_bug.do?bug_id=2147719。虽然Netty 4.0中也是基于JAVA NIO框架进行封装的(上文中已经给出了Netty中NioServerSocketChannel类的介绍),但是Netty已经将这个bug进行了处理。

黑客/网络安全学习包

资料目录

  1. 成长路线图&学习规划

  2. 配套视频教程

  3. SRC&黑客文籍

  4. 护网行动资料

  5. 黑客必读书单

  6. 面试题合集

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

1.成长路线图&学习规划

要学习一门新的技术,作为新手一定要先学习成长路线图方向不对,努力白费

对于从来没有接触过网络安全的同学,我们帮你准备了详细的学习成长路线图&学习规划。可以说是最科学最系统的学习路线,大家跟着这个大的方向学习准没问题。


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

2.视频教程

很多朋友都不喜欢晦涩的文字,我也为大家准备了视频教程,其中一共有21个章节,每个章节都是当前板块的精华浓缩


因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

3.SRC&黑客文籍

大家最喜欢也是最关心的SRC技术文籍&黑客技术也有收录

SRC技术文籍:

黑客资料由于是敏感资源,这里不能直接展示哦!

4.护网行动资料

其中关于HW护网行动,也准备了对应的资料,这些内容可相当于比赛的金手指!

5.黑客必读书单

**

**

6.面试题合集

当你自学到这里,你就要开始思考找工作的事情了,而工作绕不开的就是真题和面试题。

更多内容为防止和谐,可以扫描获取~

因篇幅有限,仅展示部分资料,需要点击下方链接即可前往获取

CSDN大礼包:《黑客&网络安全入门&进阶学习资源包》免费分享

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值