参考:
https://www.cnblogs.com/study-makes-me-happy/p/9603290.html
https://blog.csdn.net/u013851082/article/details/53942947
https://blog.csdn.net/lisha006/article/details/82856906
https://www.cnblogs.com/sxkgeek/p/9488703.html
https://www.cnblogs.com/liushui-sky/p/12917347.html
IO、NIO、AIO理解
摘要: 关于BIO和NIO的理解
最近大概看了ZooKeeper和Mina的源码发现都是用Java NIO实现的,所以有必要搞清楚什么是NIO。下面是我结合网络资料自己总结的,为了节约时间图示随便画的,能达意就行。
简介:
BIO:同步阻塞式IO,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。NIO:同步非阻塞式IO,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。
AIO(NIO.2):异步非阻塞式IO,服务器实现模式为一个有效请求一个线程,客户端的I/O请求都是由OS先完成了再通知服务器应用去启动线程进行处理。
BIO
同步阻塞式IO,相信每一个学习过操作系统网络编程或者任何语言的网络编程的人都很熟悉,在while循环中服务端会调用accept方法等待接收客户端的连接请求,一旦接收到一个连接请求,就可以建立通信套接字在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接的客户端的操作执行完成。
如果BIO要能够同时处理多个客户端请求,就必须使用多线程,即每次accept阻塞等待来自客户端请求,一旦受到连接请求就建立通信套接字同时开启一个新的线程来处理这个套接字的数据读写请求,然后立刻又继续accept等待其他客户端连接请求,即为每一个客户端连接请求都创建一个线程来单独处理,大概原理图就像这样:
虽然此时服务器具备了高并发能力,即能够同时处理多个客户端请求了,但是却带来了一个问题,随着开启的线程数目增多,将会消耗过多的内存资源,导致服务器变慢甚至崩溃,NIO可以一定程度解决这个问题。
NIO
同步非阻塞式IO,关键是采用了事件驱动的思想来实现了一个多路转换器。
NIO与BIO最大的区别就是只需要开启一个线程就可以处理来自多个客户端的IO事件,这是怎么做到的呢?
就是多路复用器,可以监听来自多个客户端的IO事件:
A. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。
B. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。
C. 监听多个客户端的连接请求和接收数据请求同时还能监听自己时候有数据要发送。
总之就是在一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理。
各自应用场景
到这里你也许已经发现,一旦有请求到来(不管是几个同时到还是只有一个到),都会调用对应IO处理函数处理,所以:
(1)NIO适合处理连接数目特别多,但是连接比较短(轻操作)的场景,Jetty,Mina,ZooKeeper等都是基于java nio实现。
(2)BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中。
(3)AIO新的IO2.0,即NIO2.0,jdk1.7开始应用,叫做异步不阻塞的IO。AIO引入异常通道的概念,采用了Proactor模式,简化了程序编写,一个有效的请求才启动一个线程,它的特点是先由操作系统完成后才通知服务端程序启动线程去处理,一般适用于连接数较多且连接时间长的应用。
附录:下面附上一个别人写的java NIO的例子。
服务端:
- 1. package cn.nio;
- 2.
- 3. import java.io.IOException;
- 4. import java.net.InetSocketAddress;
- 5. import java.nio.ByteBuffer;
- 6. import java.nio.channels.SelectionKey;
- 7. import java.nio.channels.Selector;
- 8. import java.nio.channels.ServerSocketChannel;
- 9. import java.nio.channels.SocketChannel;
- 10. import java.util.Iterator;
- 11.
- 12. /**
- 13. * NIO服务端
- 14. *
- 15. */
- 16. public class NIOServer {
- 17. //通道管理器
- 18. private Selector selector;
- 19.
- 20. /**
- 21. * 获得一个ServerSocket通道,并对该通道做一些初始化的工作
- 22. * @param port 绑定的端口号
- 23. * @throws IOException
- 24. */
- 25. public void initServer(int port) throws IOException {
- 26. // 获得一个ServerSocket通道
- 27. ServerSocketChannel serverChannel = ServerSocketChannel.open();
- 28. // 设置通道为非阻塞
- 29. serverChannel.configureBlocking(false);
- 30. // 将该通道对应的ServerSocket绑定到port端口
- 31. serverChannel.socket().bind(new InetSocketAddress(port));
- 32. // 获得一个通道管理器
- 33. this.selector = Selector.open();
- 34. //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_ACCEPT事件,注册该事件后,
- 35. //当该事件到达时,selector.select()会返回,如果该事件没到达selector.select()会一直阻塞。
- 36. serverChannel.register(selector, SelectionKey.OP_ACCEPT);
- 37. }
- 38.
- 39. /**
- 40. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
- 41. * @throws IOException
- 42. */
- 43. @SuppressWarnings("unchecked")
- 44. public void listen() throws IOException {
- 45. System.out.println("服务端启动成功!");
- 46. // 轮询访问selector
- 47. while (true) {
- 48. //当注册的事件到达时,方法返回;否则,该方法会一直阻塞
- 49. selector.select();
- 50. // 获得selector中选中的项的迭代器,选中的项为注册的事件
- 51. Iterator ite = this.selector.selectedKeys().iterator();
- 52. while (ite.hasNext()) {
- 53. SelectionKey key = (SelectionKey) ite.next();
- 54. // 删除已选的key,以防重复处理
- 55. ite.remove();
- 56. // 客户端请求连接事件
- 57. if (key.isAcceptable()) {
- 58. ServerSocketChannel server = (ServerSocketChannel) key
- 59. .channel();
- 60. // 获得和客户端连接的通道
- 61. SocketChannel channel = server.accept();
- 62. // 设置成非阻塞
- 63. channel.configureBlocking(false);
- 64.
- 65. //在这里可以给客户端发送信息哦
- 66. channel.write(ByteBuffer.wrap(new String("向客户端发送了一条信息").getBytes()));
- 67. //在和客户端连接成功之后,为了可以接收到客户端的信息,需要给通道设置读的权限。
- 68. channel.register(this.selector, SelectionKey.OP_READ);
- 69.
- 70. // 获得了可读的事件
- 71. } else if (key.isReadable()) {
- 72. read(key);
- 73. }
- 74.
- 75. }
- 76.
- 77. }
- 78. }
- 79. /**
- 80. * 处理读取客户端发来的信息 的事件
- 81. * @param key
- 82. * @throws IOException
- 83. */
- 84. public void read(SelectionKey key) throws IOException{
- 85. // 服务器可读取消息:得到事件发生的Socket通道
- 86. SocketChannel channel = (SocketChannel) key.channel();
- 87. // 创建读取的缓冲区
- 88. ByteBuffer buffer = ByteBuffer.allocate(10);
- 89. channel.read(buffer);
- 90. byte[] data = buffer.array();
- 91. String msg = new String(data).trim();
- 92. System.out.println("服务端收到信息:"+msg);
- 93. ByteBuffer outBuffer = ByteBuffer.wrap(msg.getBytes());
- 94. channel.write(outBuffer);// 将消息回送给客户端
- 95. }
- 96.
- 97. /**
- 98. * 启动服务端测试
- 99. * @throws IOException
- 100. */
- 101. public static void main(String[] args) throws IOException {
- 102. NIOServer server = new NIOServer();
- 103. server.initServer(8000);
- 104. server.listen();
- 105. }
- 106.
- 107. }
客户端:
- 1. package cn.nio;
- 2.
- 3. import java.io.IOException;
- 4. import java.net.InetSocketAddress;
- 5. import java.nio.ByteBuffer;
- 6. import java.nio.channels.SelectionKey;
- 7. import java.nio.channels.Selector;
- 8. import java.nio.channels.SocketChannel;
- 9. import java.util.Iterator;
- 10.
- 11. /**
- 12. * NIO客户端
- 13. *
- 14. */
- 15. public class NIOClient {
- 16. //通道管理器
- 17. private Selector selector;
- 18.
- 19. /**
- 20. * 获得一个Socket通道,并对该通道做一些初始化的工作
- 21. * @param ip 连接的服务器的ip
- 22. * @param port 连接的服务器的端口号
- 23. * @throws IOException
- 24. */
- 25. public void initClient(String ip,int port) throws IOException {
- 26. // 获得一个Socket通道
- 27. SocketChannel channel = SocketChannel.open();
- 28. // 设置通道为非阻塞
- 29. channel.configureBlocking(false);
- 30. // 获得一个通道管理器
- 31. this.selector = Selector.open();
- 32.
- 33. // 客户端连接服务器,其实方法执行并没有实现连接,需要在listen()方法中调
- 34. //用channel.finishConnect();才能完成连接
- 35. channel.connect(new InetSocketAddress(ip,port));
- 36. //将通道管理器和该通道绑定,并为该通道注册SelectionKey.OP_CONNECT事件。
- 37. channel.register(selector, SelectionKey.OP_CONNECT);
- 38. }
- 39.
- 40. /**
- 41. * 采用轮询的方式监听selector上是否有需要处理的事件,如果有,则进行处理
- 42. * @throws IOException
- 43. */
- 44. @SuppressWarnings("unchecked")
- 45. public void listen() throws IOException {
- 46. // 轮询访问selector
- 47. while (true) {
- 48. selector.select();
- 49. // 获得selector中选中的项的迭代器
- 50. Iterator ite = this.selector.selectedKeys().iterator();
- 51. while (ite.hasNext()) {
- 52. SelectionKey key = (SelectionKey) ite.next();
- 53. // 删除已选的key,以防重复处理
- 54. ite.remove();
- 55. // 连接事件发生
- 56. if (key.isConnectable()) {
- 57. SocketChannel channel = (SocketChannel) key
- 58. .channel();
- 59. // 如果正在连接,则完成连接
- 60. if(channel.isConnectionPending()){
- 61. channel.finishConnect();
- 62.
- 63. }
- 64. // 设置成非阻塞
- 65. channel.configureBlocking(false);
- 66.
- 67. //在这里可以给服务端发送信息哦
- 68. channel.write(ByteBuffer.wrap(new String("向服务端发送了一条信息").getBytes()));
- 69. //在和服务端连接成功之后,为了可以接收到服务端的信息,需要给通道设置读的权限。
- 70. channel.register(this.selector, SelectionKey.OP_READ);
- 71.
- 72. // 获得了可读的事件
- 73. } else if (key.isReadable()) {
- 74. read(key);
- 75. }
- 76.
- 77. }
- 78.
- 79. }
- 80. }
- 81. /**
- 82. * 处理读取服务端发来的信息 的事件
- 83. * @param key
- 84. * @throws IOException
- 85. */
- 86. public void read(SelectionKey key) throws IOException{
- 87. //和服务端的read方法一样
- 88. }
- 89.
- 90.
- 91. /**
- 92. * 启动客户端测试
- 93. * @throws IOException
- 94. */
- 95. public static void main(String[] args) throws IOException {
- 96. NIOClient client = new NIOClient();
- 97. client.initClient("localhost",8000);
- 98. client.listen();
- 99. }
- 100.
- 101. }
NIo、Bio、aio、 的原理及区别与应用场景
在高性能的IO体系设计中,有几个名词概念常常会使我们感到迷惑不解。具体如下:
序号 | 问题 |
1 | 什么是同步? |
2 | 什么是异步? |
3 | 什么是阻塞? |
4 | 什么是非阻塞? |
5 | 什么是同步阻塞? |
6 | 什么是同步非阻塞? |
7 | 什么是异步阻塞? |
8 | 什么是异步非阻塞? |
散仙不才,在查了一部分资料后,愿试着以通俗易懂的方式解释下这几个名词。如有不足之处,还望告知。
在弄清楚上面的几个问题之前,我们首先得明白什么是同步,异步,阻塞,非阻塞,只有这几个单个概念理解清楚了,然后在组合理解起来,就相对比较容易了。
1,同步和异步是针对应用程序和内核的交互而言的。 同步/异步是在时间上强调处理事情的结果/机会成本的两种处理策略;强调结果意味着对结果的迫不急待,不过结果是正确的还是错误的,反正你要立即给我一个结果响应;强调时间机会成本意味着对等待结果浪费的时间极其难接受,而对结果并不是那么急切,暂时不管结果(让处理方处理完主动通知结果/自己空闲的时候主动去获取结果)转而去处理其他事情
2,阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
3,同步/异步是宏观上(进程间通讯,通常表现为网络IO的处理上),阻塞/非阻塞是微观上(进程内数据传输,通常表现为对本地IO的处理上);阻塞和非阻塞是同步/异步的表现形式
由上描述基本可以总结一句简短的话,同步和异步是目的,阻塞和非阻塞是实现方式。
编号 | 名词 | 解释 | 举例 |
1 | 同步 | 指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪 | 自己上街买衣服,自己亲自干这件事,别的事干不了。 |
2 | 异步 | 异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知(异步的特点就是通知) | 告诉朋友自己合适衣服的尺寸,大小,颜色,让朋友委托去卖,然后自己可以去干别的事。(使用异步IO时,Java将IO读写委托给OS处理,需要将数据缓冲区地址和大小传给OS) |
3 | 阻塞 | 所谓阻塞方式的意思是指, 当试图对该文件描述符进行读写时, 如果当时没有东西可读,或者暂时不可写, 程序就进入等待 状态, 直到有东西可读或者可写为止 | 去公交站充值,发现这个时候,充值员不在(可能上厕所去了),然后我们就在这里等待,一直等到充值员回来为止。(当然现实社会,可不是这样,但是在计算机里确实如此。) |
4 | 非阻塞 | 非阻塞状态下, 如果没有东西可读, 或者不可写, 读写函数马上返回, 而不会等待, | 银行里取款办业务时,领取一张小票,领取完后我们自己可以玩玩手机,或者与别人聊聊天,当轮我们时,银行的喇叭会通知,这时候我们就可以去了。 |
下面我们再来理解组合方式的IO类型,就好理解多了。
同步阻塞IO(JAVA BIO):
同步并阻塞,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器端就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然可以通过线程池机制改善。
同步非阻塞IO(Java NIO) : 同步非阻塞,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有I/O请求时才启动一个线程进行处理。用户进程也需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问。
异步阻塞IO(Java NIO):
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序,这其实就是同步和异步最关键的区别,同步必须等待或者主动的去询问IO是否完成,那么为什么说是阻塞的呢?因为此时是通过select系统调用来完成的,而select函数本身的实现方式是阻塞的,而采用select函数有个好处就是它可以同时监听多个文件句柄(如果从UNP的角度看,select属于同步操作。因为select之后,进程还需要读写数据),从而提高系统的并发性!
(Java AIO(NIO.2))异步非阻塞IO:
在此种模式下,用户进程只需要发起一个IO操作然后立即返回,等IO操作真正的完成以后,应用程序会得到IO操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的IO读写操作,因为真正的IO读取或者写入操作已经由内核完成了。
BIO、NIO、AIO适用场景分析:
BIO方式适用于连接数目比较小且固定的架构,这种方式对服务器资源要求比较高,并发局限于应用中,JDK1.4以前的唯一选择,但程序直观简单易理解。
NIO方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,JDK1.4开始支持。
AIO方式使用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂,JDK7开始支持。
搞清楚了以上概念以后,我们再回过头来看看,Reactor模式和Proactor模式。
(其实阻塞与非阻塞都可以理解为同步范畴下才有的概念,对于异步,就不会再去分阻塞非阻塞。对于用户进程,接到异步通知后,就直接操作进程用户态空间里的数据好了。)
首先来看看Reactor模式,Reactor模式应用于同步I/O的场景。我们分别以读操作和写操作为例来看看Reactor中的具体步骤:
读取操作:
1. 应用程序注册读就绪事件和相关联的事件处理器
2. 事件分离器等待事件的发生
3. 当发生读就绪事件的时候,事件分离器调用第一步注册的事件处理器
4. 事件处理器首先执行实际的读取操作,然后根据读取到的内容进行进一步的处理
写入操作类似于读取操作,只不过第一步注册的是写就绪事件。
下面我们来看看Proactor模式中读取操作和写入操作的过程:
读取操作:
1. 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。
2. 事件分离器等待读取操作完成事件
3. 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作(异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作,操作系统扮演了重要角色),并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。
4. 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。
Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。
从上面可以看出,Reactor和Proactor模式的主要区别就是真正的读取和写入操作是有谁来完成的,Reactor中需要应用程序自己读取或者写入数据,而Proactor模式中,应用程序不需要进行实际的读写过程,它只需要从缓存区读取或者写入即可,操作系统会读取缓存区或者写入缓存区到真正的IO设备.
综上所述,同步和异步是相对于应用和内核的交互方式而言的,同步 需要主动去询问,而异步的时候内核在IO事件发生的时候通知应用程序,而阻塞和非阻塞仅仅是系统在调用系统调用的时候函数的实现方式而已。
如果你想吃一份宫保鸡丁盖饭:
同步阻塞:你到饭馆点餐,然后在那等着,还要一边喊:好了没啊!
同步非阻塞:在饭馆点完餐,就去遛狗了。不过溜一会儿,就回饭馆喊一声:好了没啊!
异步阻塞:遛狗的时候,接到饭馆电话,说饭做好了,让您亲自去拿。
异步非阻塞:饭馆打电话说,我们知道您的位置,一会给你送过来,安心遛狗就可以了。
“一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作。
同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO。
阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
同步和异步是针对应用程序和内核的交互而言的,同步指的是用户进程触发IO操作并等待或者轮询的去查看IO操作是否就绪,而异步是指用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。而阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式,说白了是一种读取或者写入操作函数的实现方式,阻塞方式下读取或者写入函数将一直等待,而非阻塞方式下,读取或者写入函数会立即返回一个状态值。
所以,IO操作可以分为3类:同步阻塞(即早期的IO操作)、同步非阻塞(NIO)、异步(AIO)。
同步阻塞:
在此种方式下,用户进程在发起一个IO操作以后,必须等待IO操作的完成,只有当真正完成了IO操作以后,用户进程才能运行。JAVA传统的IO模型属于此种方式。
同步非阻塞:
在此种方式下,用户进程发起一个IO操作以后边可返回做其它事情,但是用户进程需要时不时的询问IO操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。其中目前JAVA的NIO就属于同步非阻塞IO。
异步:
此种方式下是指应用发起一个IO操作以后,不等待内核IO操作的完成,等内核完成IO操作以后会通知应用程序。”
这段话比较清楚
参考:http://blog.csdn.net/brainkick/article/details/9312407
原文:http://my.oschina.net/bluesky0leon/blog/132361
关于BIO | NIO | AIO的讨论一直存在,有时候也很容易让人混淆,就我的理解,给出一个解释:
BIO | NIO | AIO,本身的描述都是在Java语言的基础上的。而描述IO,我们需要从两个层面:
- 编程语言
- 实现原理
- 底层基础
从编程语言层面
BIO | NIO | AIO 以Java的角度,理解,linux c里也有AIO的概念(库),这些概念不知道什么原因被炒火起来,这里只从Java角度入手。
- BIO,同步阻塞式IO,简单理解:一个连接一个线程
- NIO,同步非阻塞IO,简单理解:一个请求一个线程
- AIO,异步非阻塞IO,简单理解:一个有效请求一个线程
BIO
在JDK1.4之前,用Java编写网络请求,都是建立一个ServerSocket,然后,客户端建立Socket时就会询问是否有线程可以处理,如果没有,要么等待,要么被拒绝。即:一个连接,要求Server对应一个处理线程。
NIO
在Java里的由来,在JDK1.4及以后版本中提供了一套API来专门操作非阻塞I/O,我们可以在java.nio包及其子包中找到相关的类和接口。由于这套API是JDK新提供的I/O API,因此,也叫New I/O,这就是包名nio的由来。这套API由三个主要的部分组成:缓冲区(Buffers)、通道(Channels)和非阻塞I/O的核心类组成。在理解NIO的时候,需要区分,说的是New I/O还是非阻塞IO,New I/O是Java的包,NIO是非阻塞IO概念。这里讲的是后面一种。
NIO本身是基于事件驱动思想来完成的,其主要想解决的是BIO的大并发问题: 在使用同步I/O的网络应用中,如果要同时处理多个客户端请求,或是在客户端要同时和多个服务器进行通讯,就必须使用多线程来处理。也就是说,将每一个客户端请求分配给一个线程来单独处理。这样做虽然可以达到我们的要求,但同时又会带来另外一个问题。由于每创建一个线程,就要为这个线程分配一定的内存空间(也叫工作存储器),而且操作系统本身也对线程的总数有一定的限制。如果客户端的请求过多,服务端程序可能会因为不堪重负而拒绝客户端的请求,甚至服务器可能会因此而瘫痪。
NIO基于Reactor,当socket有流可读或可写入socket时,操作系统会相应的通知引用程序进行处理,应用再将流读取到缓冲区或写入操作系统。
也就是说,这个时候,已经不是一个连接就要对应一个处理线程了,而是有效的请求,对应一个线程,当连接没有数据时,是没有工作线程来处理的。
AIO
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。
即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。
在JDK1.7中,这部分内容被称作NIO.2,主要在java.nio.channels包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
其中的read/write方法,会返回一个带回调函数的对象,当执行完读取/写入操作后,直接调用回调函数。
实现原理
说道实现原理,还要从操作系统的IO模型上了解
按照《Unix网络编程》的划分,IO模型可以分为:阻塞IO、非阻塞IO、IO复用、信号驱动IO和异步IO,按照POSIX标准来划分只分为两类:同步IO和异步IO。如何区分呢?首先一个IO操作其实分成了两个步骤:发起IO请求和实际的IO操作,同步IO和异步IO的区别就在于第二个步骤是否阻塞,如果实际的IO读写阻塞请求进程,那么就是同步IO,因此阻塞IO、非阻塞IO、IO复用、信号驱动IO都是同步IO,如果不阻塞,而是操作系统帮你做完IO操作再将结果返回给你,那么就是异步IO。阻塞IO和非阻塞IO的区别在于第一步,发起IO请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞IO,如果不阻塞,那么就是非阻塞IO。
收到操作系统的IO模型,又不得不提select/poll/epoll/iocp,关于这四个的理解,不多做解释,自己还没理解到位。
可以理解的说明是:在Linux 2.6以后,java NIO的实现,是通过epoll来实现的,这点可以通过jdk的源代码发现。而AIO,在windows上是通过IOCP实现的,在linux上还是通过epoll来实现的。
这里强调一点:AIO,这是I/O处理模式,而epoll等都是实现AIO的一种编程模型;换句话说,AIO是一种接口标准,各家操作系统可以实现也可以不实现。在不同操作系统上在高并发情况下最好都采用操作系统推荐的方式。Linux上还没有真正实现网络方式的AIO。
底层基础
说到底层,要说Linux系统编程,这里自己也不熟悉,有待后来人补充了。
只笼统的说一个:AIO实现
在windows上,AIO的实现是通过IOCP来完成的,看JDK的源代码,可以发现
WindowsAsynchronousSocketChannelImpl
看实现接口:
implements Iocp.OverlappedChannel
再看实现方法:里面的read0/write0方法是native方法,调用的jvm底层实现,虚拟机技术不熟悉,不献丑了。
在linux上,AIO的实现是通过epoll来完成的,看JDK源码,可以发现,实现源码是:
UnixAsynchronousSocketChannelImpl
看实现接口:
implements Port.PollableChannel
这是与windows最大的区别,poll的实现,在linux2.6后,默认使用epoll。
这样就可以理解了。
写在最后:Java开发为基础的,对于操作系统底层的认知是没有C语言为基础的大牛好的,语言决定了思维方式,古人诚不欺我
最后,几篇解释的不错的文章:
BIO、NIO、AIO 区别和应用场景
前边简单介绍过IO的基本情况 Java IO流
简单回顾
对于IO我们应该非常熟悉了,IO不仅仅针对文件的操作,网络编程socket的通信,就是IO操作。
输入、输出流(InputStream、OutputStream)用于读取或写入字节,如操作图片、视频等。
Reader和Writer 则用于操作字符,增加了字符编码功能。本质上计算机操作都是字节,不管是网络或者文件,Reader和Writer等于构建了应用逻辑和原始数据的另一层通道。
BufferedOutputStream、BufferedInputStream等带有缓冲区的实现,可以避免频繁的磁盘操作,通过设计缓冲区将批量数据进行一次操作。
NIO
能解决什么问题?
为什么要有NIO,NIO是什么?
首先看一下BIO,如果有一台服务器,能承受简单的客户端请求,那么使用io和net中的同步、阻塞式API应该是可以实现了。但是为了一个用户的请求而单独启动一个线程,开销应该不小吧。java语言对线程的实现是比较重量的,启动或销毁线程,都会有明显开销,每个线程都有单独的线程棧占用明显的内存。引入线程池,就能很大程度的避免不必要的开销。
这种情况适合连接数并不多,只有最多几百个连接的普通应用,能比较好的进行工作,但如果连接数量剧增,这种实现方式就无法很好的工作了,对于并发量要求较高的企业,这种方案,肯定是不可取的。
NIO采用的是一种多路复用的机制,利用单线程轮询事件,高效定位就绪的Channel来决定做什么,只是Select阶段是阻塞式的,能有效避免大量连接数时,频繁线程的切换带来的性能或各种问题。
上图随便画的,只是方便理解,并不能作为实现的具体的参考。
首先,Requester方通过Selector.open()创建了一个Selector准备好了调度角色。
创建了SocketChannel(ServerSocketChannel) 并注册到Selector中,通过设置key(SelectionKey)告诉调度者所应该关注的连接请求。
阻塞,Selector阻塞在select操作中,如果发现有Channel发生连接请求,就会唤醒处理请求。
NIO同步非阻塞式IO
对比BIO的同步阻塞IO操作,实际上NIO是同步非阻塞IO,一个线程在同步的进行轮询检查,Selector不断轮询注册在其上的Channel,某个Channel上面发生读写连接请求,这个Channel就处于就绪状态,被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
同步和异步说的是消息的通知机制,这个线程仍然要定时的读取stream,判断数据有没有准备好,client采用循环的方式去读取(线程自己去抓去信息),CPU被浪费。
非阻塞:体现在,这个线程可以去干别的,不需要一直在这等着。Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,没有最大连接句柄限制。所以只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。
AIO
是在NIO的基础上引入异步通道的概念,实现异步非阻塞式的IO处理。如下图(网络截图):
AIO不需要通过多路复用器对注册的通道进行轮询操作即可实现异步读写。什么意思呢?NIO采用轮询的方式,一直在轮询的询问stream中数据是否准备就绪,如果准备就绪发起处理。但是AIO就不需要了,AIO框架在windows下使用windows IOCP技术,在Linux下使用epoll多路复用IO技术模拟异步IO, 即:应用程序向操作系统注册IO监听,然后继续做自己的事情。操作系统发生IO事件,并且准备好数据后,在主动通知应用程序,触发相应的函数(这就是一种以订阅者模式进行的改造)。由于应用程序不是“轮询”方式而是订阅-通知方式,所以不再需要selector轮询,由channel通道直接到操作系统注册监听。
NIO(AIO)中几个概念
缓冲区 Buffer
NIO基于块进行数据处理,在NIO中所有数据的读取都是通过缓冲Buffer进行处理。
具体的缓存区有这些:ByteBuffe、CharBuffer、 ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。他们实现了相同的接口:Buffer。
通道 Channel
对数据的读取和写入要通过Channel通道。通道不同于流的地方就是通道是双向的,用于读、写和同时读写操作。底层的操作系统的通道一般都是全双工的,全双工的Channel比流能更好的映射底层操作系统的API。
多路复用器 Selector
Selector提供选择已经就绪的任务的能力:
Selector轮询注册在其上的Channel,如果某个Channel发生读写请求并且Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。(同步)
一个Selector可以同时轮询多个Channel,因为JDK使用了epoll()代替传统的select实现,所以没有最大连接句柄1024/2048的限制。所以,只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。(非阻塞)
NIO和AIO
NIO:会等数据准备好后,再交由应用进行处理,数据的读取/写入过程依然在应用线程中完成,只是将等待的时间剥离到单独的线程中去,节省了数据准备时间,因为多路复用机制,Selector会得到复用,对于那些读写过程时间长的,NIO就不太适合。
AIO:读完(内核内存拷贝到用户内存)了系统再通知应用,使用回调函数,进行业务处理,AIO能够胜任那些重量级,读写过程长的任务。
java IO、NIO、AIO详解
正文
回到顶部概述
在我们学习Java的IO流之前,我们都要了解几个关键词
- 同步与异步(synchronous/asynchronous):同步是一种可靠的有序运行机制,当我们进行同步操作时,后续的任务是等待当前调用返回,才会进行下一步;而异步则相反,其他任务不需要等待当前调用返回,通常依靠事件、回调等机制来实现任务间次序关系
- 阻塞与非阻塞:在进行阻塞操作时,当前线程会处于阻塞状态,无法从事其他任务,只有当条件就绪才能继续,比如ServerSocket新连接建立完毕,或者数据读取、写入操作完成;而非阻塞则是不管IO操作是否结束,直接返回,相应操作在后台继续处理
同步和异步的概念:实际的I/O操作
同步是用户线程发起I/O请求后需要等待或者轮询内核I/O操作完成后才能继续执行
异步是用户线程发起I/O请求后仍需要继续执行,当内核I/O操作完成后会通知用户线程,或者调用用户线程注册的回调函数
阻塞和非阻塞的概念:发起I/O请求
阻塞是指I/O操作需要彻底完成后才能返回用户空间
非阻塞是指I/O操作被调用后立即返回一个状态值,无需等I/O操作彻底完成
BIO、NIO、AIO的概述
首先,传统的 java.io包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。
java.io包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。
很多时候,人们也把 java.net下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。
第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。
回到顶部一、IO流(同步、阻塞)
1、概述
IO流简单来说就是input和output流,IO流主要是用来处理设备之间的数据传输,Java IO对于数据的操作都是通过流实现的,而java用于操作流的对象都在IO包中。
2、分类
按操作数据分为:字节流(InputStream、OutputStream)和字符流(Reader、Writer)
按流向分:输入流(Reader、InputStream)和输出流(Writer、OutputStream)
3、字符流
概述
只用来处理文本数据
数据最常见的表现形式是文件,字符流用来操作文件的子类一般是FileReader和FileWriter
字符流读写文件注意事项:
- 写入文件必须要用flush()刷新
- 用完流记得要关闭流
- 使用流对象要抛出IO异常
- 定义文件路径时,可以用"/"或者"\"
- 在创建一个文件时,如果目录下有同名文件将被覆盖
- 在读取文件时,必须保证该文件已存在,否则抛出异常
字符流的缓冲区
- 缓冲区的出现是为了提高流的操作效率而出现的
- 需要被提高效率的流作为参数传递给缓冲区的构造函数
- 在缓冲区中封装了一个数组,存入数据后一次取出
4、字节流
概述
用来处理媒体数据
字节流读写文件注意事项:
- 字节流和字符流的基本操作是相同的,但是想要操作媒体流就需要用到字节流
- 字节流因为操作的是字节,所以可以用来操作媒体文件(媒体文件也是以字节存储的)
- 输入流(InputStream)、输出流(OutputStream)
- 字节流操作可以不用刷新流操作
- InputStream特有方法:int available()(返回文件中的字节个数)
字节流的缓冲区
字节流缓冲区跟字符流缓冲区一样,也是为了提高效率
5、Java Scanner类
Java 5添加了java.util.Scanner类,这是一个用于扫描输入文本的新的实用程序
关于nextInt()、next()、nextLine()的理解
nextInt():只能读取数值,若是格式不对,会抛出java.util.InputMismatchException异常
next():遇见第一个有效字符(非空格,非换行符)时,开始扫描,当遇见第一个分隔符或结束符(空格或换行符)时,结束扫描,获取扫描到的内容
nextLine():可以扫描到一行内容并作为字符串而被捕获到
关于hasNext()、hasNextLine()、hasNextxxx()的理解
就是为了判断输入行中是否还存在xxx的意思
与delimiter()有关的方法
应该是输入内容的分隔符设置,
回到顶部二、NIO(同步、非阻塞)
NIO之所以是同步,是因为它的accept/read/write方法的内核I/O操作都会阻塞当前线程
首先,我们要先了解一下NIO的三个主要组成部分:Channel(通道)、Buffer(缓冲区)、Selector(选择器)
(1)Channel(通道)
Channel(通道):Channel是一个对象,可以通过它读取和写入数据。可以把它看做是IO中的流,不同的是:
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过buffer对象
正如上面提到的,所有数据都通过Buffer对象处理,所以,您永远不会将字节直接写入到Channel中,相反,您是将数据写入到Buffer中;同样,您也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。
因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。特别是在Unix模型中,底层操作系统通常都是双向的。
在Java NIO中的Channel主要有如下几种类型:
- FileChannel:从文件读取数据的
- DatagramChannel:读写UDP网络协议数据
- SocketChannel:读写TCP网络协议数据
- ServerSocketChannel:可以监听TCP连接
(2)Buffer
Buffer是一个对象,它包含一些要写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。
在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。
使用 Buffer 读写数据一般遵循以下四个步骤:
1.写入数据到 Buffer;
2.调用 flip() 方法;
3.从 Buffer 中读取数据;
4.调用 clear() 方法或者 compact() 方法。
当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
Buffer主要有如下几种:
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
copyFile实例(NIO)
CopyFile是一个非常好的读写结合的例子,我们将通过CopyFile这个实力让大家体会NIO的操作过程。CopyFile执行三个基本的操作:创建一个Buffer,然后从源文件读取数据到缓冲区,然后再将缓冲区写入目标文件。
public static void copyFileUseNIO(String src,String dst) throws IOException{
//声明源文件和目标文件
FileInputStream fi=new FileInputStream(new File(src));
FileOutputStream fo=new FileOutputStream(new File(dst));
//获得传输通道channel
FileChannel inChannel=fi.getChannel();
FileChannel outChannel=fo.getChannel();
//获得容器buffer
ByteBuffer buffer=ByteBuffer.allocate(1024);
while(true){
//判断是否读完文件
int eof =inChannel.read(buffer);
if(eof==-1){
break;
}
//重设一下buffer的position=0,limit=position
buffer.flip();
//开始写
outChannel.write(buffer);
//写完要重置buffer,重设position=0,limit=capacity
buffer.clear();
}
inChannel.close();
outChannel.close();
fi.close();
fo.close();
}
(三)Selector(选择器对象)
首先需要了解一件事情就是线程上下文切换开销会在高并发时变得很明显,这是同步阻塞方式的低扩展性劣势。
Selector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了。
selector优点
有了Selector,我们就可以利用一个线程来处理所有的channels。线程之间的切换对操作系统来说代价是很高的,并且每个线程也会占用一定的系统资源。所以,对系统来说使用的线程越少越好。
1.如何创建一个Selector
Selector 就是您注册对各种 I/O 事件兴趣的地方,而且当那些事件发生时,就是这个对象告诉您所发生的事件。
Selector selector = Selector.open();
2.注册Channel到Selector
为了能让Channel和Selector配合使用,我们需要把Channel注册到Selector上。通过调用 channel.register()方法来实现注册:
channel.configureBlocking(false);
SelectionKey key =channel.register(selector,SelectionKey.OP_READ);
注意,注册的Channel 必须设置成异步模式 才可以,否则异步IO就无法工作,这就意味着我们不能把一个FileChannel注册到Selector,因为FileChannel没有异步模式,但是网络编程中的SocketChannel是可以的。
3.关于SelectionKey
请注意对register()的调用的返回值是一个SelectionKey。 SelectionKey 代表这个通道在此 Selector 上注册。当某个 Selector 通知您某个传入事件时,它是通过提供对应于该事件的 SelectionKey 来进行的。SelectionKey 还可以用于取消通道的注册。
SelectionKey中包含如下属性:
- The interest set
- The ready set
- The Channel
- The Selector
- An attached object (optional)
(1)Interest set
就像我们在前面讲到的把Channel注册到Selector来监听感兴趣的事件,interest set就是你要选择的感兴趣的事件的集合。你可以通过SelectionKey对象来读写interest set:
int interestSet = selectionKey.interestOps();
boolean isInterestedInAccept = interestSet & SelectionKey.OP_ACCEPT;
boolean isInterestedInConnect = interestSet & SelectionKey.OP_CONNECT;
boolean isInterestedInRead = interestSet & SelectionKey.OP_READ;
boolean isInterestedInWrite = interestSet & SelectionKey.OP_WRITE;
通过上面例子可以看到,我们可以通过用AND 和SelectionKey 中的常量做运算,从SelectionKey中找到我们感兴趣的事件。
(2)Ready Set
ready set 是通道已经准备就绪的操作的集合。在一次选Selection之后,你应该会首先访问这个ready set。Selection将在下一小节进行解释。可以这样访问ready集合:
int readySet = selectionKey.readyOps();
可以用像检测interest集合那样的方法,来检测Channel中什么事件或操作已经就绪。但是,也可以使用以下四个方法,它们都会返回一个布尔类型:
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable();
(3)Channel 和 Selector
我们可以通过SelectionKey获得Selector和注册的Channel:
Channel channel = selectionKey.channel();
Selector selector = selectionKey.selector();
(4)Attach一个对象
可以将一个对象或者更多信息attach 到SelectionKey上,这样就能方便的识别某个给定的通道。例如,可以附加 与通道一起使用的Buffer,或是包含聚集数据的某个对象。使用方法如下:
selectionKey.attach(theObject);
Object attachedObj = selectionKey.attachment();
还可以在用register()方法向Selector注册Channel的时候附加对象。如:
SelectionKey key = channel.register(selector, SelectionKey.OP_READ, theObject);
4.关于SelectedKeys()
生产系统中一般会额外进行就绪状态检查
一旦调用了select()方法,它就会返回一个数值,表示一个或多个通道已经就绪,然后你就可以通过调用selector.selectedKeys()方法返回的SelectionKey集合来获得就绪的Channel。请看演示方法:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
当你通过Selector注册一个Channel时,channel.register()方法会返回一个SelectionKey对象,这个对象就代表了你注册的Channel。这些对象可以通过selectedKeys()方法获得。你可以通过迭代这些selected key来获得就绪的Channel,下面是演示代码:
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while(keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if(key.isAcceptable()) {
// a connection was accepted by a ServerSocketChannel.
} else if (key.isConnectable()) {
// a connection was established with a remote server.
} else if (key.isReadable()) {
// a channel is ready for reading
} else if (key.isWritable()) {
// a channel is ready for writing
}
keyIterator.remove();
}
这个循环遍历selected key的集合中的每个key,并对每个key做测试来判断哪个Channel已经就绪。
请注意循环中最后的keyIterator.remove()方法。Selector对象并不会从自己的selected key集合中自动移除SelectionKey实例。我们需要在处理完一个Channel的时候自己去移除。当下一次Channel就绪的时候,Selector会再次把它添加到selected key集合中。
SelectionKey.channel()方法返回的Channel需要转换成你具体要处理的类型,比如是ServerSocketChannel或者SocketChannel等等。
(4)NIO多路复用
主要步骤和元素:
-
首先,通过 Selector.open() 创建一个 Selector,作为类似调度员的角色。
-
然后,创建一个 ServerSocketChannel,并且向 Selector 注册,通过指定 SelectionKey.OP_ACCEPT,告诉调度员,它关注的是新的连接请求。
-
注意,为什么我们要明确配置非阻塞模式呢?这是因为阻塞模式下,注册操作是不允许的,会抛出 IllegalBlockingModeException 异常。
-
Selector 阻塞在 select 操作,当有 Channel 发生接入请求,就会被唤醒。
-
在 具体的 方法中,通过 SocketChannel 和 Buffer 进行数据操作
IO 都是同步阻塞模式,所以需要多线程以实现多任务处理。而 NIO 则是利用了单线程轮询事件的机制,通过高效地定位就绪的 Channel,来决定做什么,仅仅 select 阶段是阻塞的,可以有效避免大量客户端连接时,频繁线程切换带来的问题,应用的扩展能力有了非常大的提高
回到顶部三、NIO2(异步、非阻塞)
AIO是异步IO的缩写,虽然NIO在网络操作中,提供了非阻塞的方法,但是NIO的IO行为还是同步的。对于NIO来说,我们的业务线程是在IO操作准备好时,得到通知,接着就由这个线程自行进行IO操作,IO操作本身是同步的。
但是对AIO来说,则更加进了一步,它不是在IO准备好时再通知线程,而是在IO操作已经完成后,再给线程发出通知。因此AIO是不会阻塞的,此时我们的业务逻辑将变成一个回调函数,等待IO操作完成后,由系统自动触发。
与NIO不同,当进行读写操作时,只须直接调用API的read或write方法即可。这两种方法均为异步的,对于读操作而言,当有流可读取时,操作系统会将可读的流传入read方法的缓冲区,并通知应用程序;对于写操作而言,当操作系统将write方法传递的流写入完毕时,操作系统主动通知应用程序。 即可以理解为,read/write方法都是异步的,完成后会主动调用回调函数。 在JDK1.7中,这部分内容被称作NIO.2,主要在Java.nio.channels包下增加了下面四个异步通道:
- AsynchronousSocketChannel
- AsynchronousServerSocketChannel
- AsynchronousFileChannel
- AsynchronousDatagramChannel
在AIO socket编程中,服务端通道是AsynchronousServerSocketChannel,这个类提供了一个open()静态工厂,一个bind()方法用于绑定服务端IP地址(还有端口号),另外还提供了accept()用于接收用户连接请求。在客户端使用的通道是AsynchronousSocketChannel,这个通道处理提供open静态工厂方法外,还提供了read和write方法。
在AIO编程中,发出一个事件(accept read write等)之后要指定事件处理类(回调函数),AIO中的事件处理类是CompletionHandler<V,A>,这个接口定义了如下两个方法,分别在异步操作成功和失败时被回调。
void completed(V result, A attachment);
void failed(Throwable exc, A attachment);
Linux的5种网络IO模型详解
linux的五种IO模型,分别是:阻塞IO、非阻塞IO、多路复用IO、信号驱动IO以及异步IO。其中阻塞IO、非阻塞IO、多路复用IO、信号驱动IO都属于同步IO。
同步IO和异步IO
同步IO:应用程序主动向内核查询是否有可用数据,如果有自己负责把数据从内核copy到用户空间。
异步IO:应用程序向内核发起读数据请求需要:(1)告诉内核数据存放位置(2)注册回调函数,当内核完成数据copy后调用回调通知应用程序取数据。
同步IO/异步IO最大区别:同步IO数据从内核空间到用户空间的copy动作是由应用程序自己完成。而异步IO则是注册回调函数并告知内核用户空间缓冲区存放地址,数据copy由内核完成。
详细介绍之前,援引网上《Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO》中的一段话,便于简单理解、记忆:
阻塞IO, 给女神发一条短信, 说我来找你了, 然后就默默的一直等着女神下楼, 这个期间除了等待你不会做其他事情, 属于备胎做法.
非阻塞IO, 给女神发短信, 如果不回, 接着再发, 一直发到女神下楼, 这个期间你除了发短信等待不会做其他事情, 属于专一做法.
IO多路复用, 是找一个宿管大妈来帮你监视下楼的女生, 这个期间你可以些其他的事情. 例如可以顺便看看其他妹子,玩玩王者荣耀, 上个厕所等等. IO复用又包括 select, poll, epoll 模式. 那么它们的区别是什么?
1) select大妈 每一个女生下楼, select大妈都不知道这个是不是你的女神, 她需要一个一个询问, 并且select大妈能力还有限, 最多一次帮你监视1024个妹子
2) poll大妈不限制盯着女生的数量, 只要是经过宿舍楼门口的女生, 都会帮你去问是不是你女神
3) epoll大妈不限制盯着女生的数量, 并且也不需要一个一个去问. 那么如何做呢? epoll大妈会为每个进宿舍楼的女生脸上贴上一个大字条,上面写上女生自己的名字, 只要女生下楼了, epoll大妈就知道这个是不是你女神了, 然后大妈再通知你。
上面这些同步IO有一个共同点就是, 当女神走出宿舍门口的时候, 你已经站在宿舍门口等着女神的, 此时你属于阻塞状态接下来是异步IO的情况:
你告诉女神我来了, 然后你就去打游戏了, 一直到女神下楼了, 发现找不见你了, 女神再给你打电话通知你, 说我下楼了, 你在哪呢? 这时候你才来到宿舍门口。 此时属于逆袭做法
以下每种模型都有三种图示和描述,各人可以选择自己容易理解的部分进行记忆。
1、阻塞IO模型
最传统的一种IO模型,即在读写数据过程中会发生阻塞现象。
当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。
代码如下:
printf("Calling recv(). \n"); ret = recv(socket, recv_buf, sizeof(recv_buf), 0); printf("Had called recv(). \n")
也许有人会说,可以采用多线程+ 阻塞IO 来解决效率问题,但是由于在多线程 + 阻塞IO 中,每个socket对应一个线程,这样会造成很大的资源占用,并且尤其是对于长连接来说,线程的资源一直不会释放,如果后面陆续有很多连接的话,就会造成性能上的瓶颈。
当用户进程调用了recvfrom这个系统调用,kernel就开始了IO的第一个阶段:准备数据(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的UDP包。这个时候kernel就要等待足够的数据到来)。这个过程需要等待,也就是说数据被拷贝到操作系统内核的缓冲区中是需要一个过程的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中拷贝到用户内存,然后kernel返回结果,用户进程才解除block的状态,重新运行起来。
所以,blocking IO的特点就是在IO执行的两个阶段都被block了。
应用程序请求内核读取数据,内核数据数据缓冲区无数据或者数据未就绪前,阻塞等待。内核系统等待数据准备就绪后,拷贝数据到用户空间,待拷贝完成后,返回结果,用户程序接触阻塞,处理数据。
2、非阻塞IO模型
当用户线程发起一个IO操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送IO操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么内核它马上就将数据拷贝到了用户线程,然后返回。
在非阻塞IO模型中,用户线程需要不断地询问内核数据是否就绪,也就说非阻塞IO不会交出CPU,而会一直占用CPU。
对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。
while(1) { printf("Calling recv(). \n"); ret = recv(socket, recv_buf, sizeof(recv_buf), 0); if (EAGAIN == ret) {continue;} else if(ret > -1) { break;} printf("Had called recv(), retry.\n"); }
Linux下,可以通过设置socket使其变为non-blocking。
当用户进程发出read操作时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个read操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么内核它马上就将数据拷贝到了用户内存,然后返回。
所以,nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有。
应用程序请求内核读取数据,内核直接返回结果,如果数据未准备就绪,则返回error,应用程序继续请求,周而复始,直到内核数据准备就绪后,当内核再次收到应用程序请求后,将数据拷贝到用户空间,待拷贝完成后返回ok,应用程序处理数据。
3、IO多路复用模型
I/O多路复用是操作系统级别的,属于linux操作系统的五种I/O模型中的一种,是操作系统级别同步非阻塞的。操作系统级别的异步I/O才是真正异步非阻塞的。(参见:https://www.zhihu.com/question/59975081/answer/837766592)
所谓I/O多路复用机制,就是说通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。这种机制的使用需要额外的功能来配合: select、poll、epoll。
- select、poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的。
- select时间复杂度O(n),它仅仅知道了,有I/O事件发生了,却并不知道是哪几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
- poll(翻译:轮询)时间复杂度O(n),poll本质上和select没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个fd对应的设备状态, 但是它没有最大连接数的限制,原因是它是基于链表来存储的.
- epoll时间复杂度O(1),epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))。
在多路复用IO模型中,会有一个内核线程不断去轮询多个socket的状态,只有当真正读写事件发生时,才真正调用实际的IO读写操作。因为在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
IO multiplexing就是我们说的select,poll,epoll,有些地方也称这种IO方式为event driven IO。select/epoll的好处就在于单个process就可以同时处理多个网络连接的IO。它的基本原理就是select,poll,epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。
当用户进程调用了select,那么整个进程就会被block,而同时,kernel会 “监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。所以,IO多路复用的特点是通过一种机制一个进程能同时等待多个文件描述符,而这些文件描述符(套接字描述符)其中的任意一个进入就绪状态,select()函数就可以返回。
这里需要使用两个system call(select 和 recvfrom),而blocking IO只调用了一个system call(recvfrom)。但是,用select的优势在于它可以同时处理多个connection。
如果处理的连接数不是很高的话,使用select/epoll的web server不一定比使用mutil-threading + blocking IO的web server性能更好,可能延迟还更大。select/epoll 的优势并不是对于单个连接能处理得更好,而是在于性能更多的连接。
应用程序请求内核读取数据,首先调用了select,内核监控select监控的所有socket,当有任何一个socket数据准备就绪后,就返回给用户进程可读,然后用户进程再次向内核发送读取指令,内核将数据拷贝到用户空间,并返回结果,用户进程获得数据后进行处理。这里关于select、poll、epoll的区别不在这里描述,参见:《Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)》、《select、poll、epoll之间的区别(搜狗面试)》
4.信号驱动IO模型
在信号驱动IO模型中,当用户线程发起一个IO请求操作,会给对应的socket注册一个信号函数,然后用户线程会继续执行,当内核数据就绪时会发送一个信号给用户线程,用户线程接收到信号之后,便在信号函数中调用IO读写操作来进行实际的IO请求操作。这个一般用于UDP中,对TCP套接口几乎是没用的,原因是该信号产生得过于频繁,并且该信号的出现并没有告诉我们发生了什么事情。
在UDP上,SIGIO信号会在下面两个事件的时候产生:
1 数据报到达套接字
2 套接字上发生错误
因此我们很容易判断SIGIO出现的时候,如果不是发生错误,那么就是有数据报到达了。
而在TCP上,由于TCP是双工的,它的信号产生过于频繁,并且信号的出现几乎没有告诉我们发生了什么事情。因此对于TCP套接字,SIGIO信号是没有什么使用的。
有关函数
#include <signal.h> int sigaction(int signum, const struct sigaction *act, struct sigaction *oldact);
关于有关内容的讲解,请参考:Linux 系统编程 学习:进程间通信-Unix IPC-信号
5、异步IO模型
前面四种IO模型实际上都属于同步IO,只有最后一种是真正的异步IO,因为无论是多路复用IO还是信号驱动模型,IO操作的第2个阶段都会引起用户线程阻塞,也就是内核进行数据拷贝的过程都会让用户线程阻塞。
导言
两种高性能IO设计模式
在传统的网络服务设计模式中,有两种比较经典的模式:多线程与线程池。
多线程
对于多线程模式,也就说来了client,服务器就会新建一个线程来处理该client的读写事件,如下图所示:
这种模式虽然处理起来简单方便,但是由于服务器为每个client的连接都采用一个线程去处理,使得资源占用非常大。因此,当连接数量达到上限时,再有用户请求连接,直接会导致资源瓶颈,严重的可能会直接导致服务器崩溃。
线程池
因此,为了解决这种一个线程对应一个客户端模式带来的问题,提出了采用线程池的方式,也就说创建一个固定大小的线程池,来一个客户端,就从线程池取一个空闲线程来处理,当客户端处理完读写操作之后,就交出对线程的占用。因此这样就避免为每一个客户端都要创建线程带来的资源浪费,使得线程可以重用。
但是线程池也有它的弊端,如果连接大多是长连接,因此可能会导致在一段时间内,线程池中的线程都被占用,那么当再有用户请求连接时,由于没有可用的空闲线程来处理,就会导致客户端连接失败,从而影响用户体验。因此,线程池比较适合大量的短连接应用。
高性能IO模型
因此便出现了下面的两种高性能IO设计模式:Reactor和Proactor。Proactor前摄器模式和Reactor反应器模式。两个模式不同的地方在于,Proactor用于异步IO,而Reactor用于同步IO。
Reactor
在Reactor模式中,会先对每个client注册感兴趣的事件,然后有一个线程专门去轮询每个client是否有事件发生,当有事件发生时,便顺序处理每个事件,当所有事件处理完之后,便再转去继续轮询,如下图所示:
从这里可以看出,多路复用IO就是采用Reactor模式。
注意,上面的图中展示的 是顺序处理每个事件,当然为了提高事件处理速度,可以通过多线程或者线程池的方式来处理事件。
Proactor
在Proactor模式中:当检测到有事件发生时,会新起一个异步操作,然后交由内核线程去处理,当内核线程完成IO操作之后,发送一个通知告知操作已完成;可以得知,异步IO模型采用的就是Proactor模式。
异步IO模型是比较理想的IO模型,在异步IO模型中,当用户线程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,它会立刻返回,说明read请求已经成功发起了,因此不会对用户线程产生任何block。然后,内核会等待数据准备完成,然后将数据拷贝到用户线程,当这一切都完成之后,内核会给用户线程发送一个信号,告诉它read操作完成了。也就说用户线程完全不需要关心实际的整个IO操作是如何进行的,只需要先发起一个请求,当接收内核返回的成功信号时表示IO操作已经完成,可以直接去使用数据了。
也就说在异步IO模型中,IO操作的两个阶段都不会阻塞用户线程,这两个阶段都是由内核自动完成,然后发送一个信号告知用户线程操作已完成。用户线程中不需要再次调用IO函数进行具体的读写。
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从kernel的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。
应用程序请求内核读取数据后不等待,执行其它事项,内核待数据就绪后直接拷贝到用户空间,并发送信号给应用程序,应用程序收到信号后处理数据。
参考:
https://www.cnblogs.com/schips/p/12583129.html Linux 网络编程的5种IO模型 总结
https://www.cnblogs.com/schips/p/12543650.html Linux 网络编程的5种IO模型:阻塞IO与非阻塞IO
https://www.cnblogs.com/schips/p/12568408.html Linux 网络编程的5种IO模型:多路复用(select/poll/epoll)
https://www.cnblogs.com/schips/p/12575493.html Linux 网络编程的5种IO模型:信号驱动IO模型
https://www.cnblogs.com/schips/p/12575933.html Linux 网络编程的5种IO模型:异步IO模型
https://www.cnblogs.com/natian-ws/p/10785649.html Linux IO模式及 select、poll、epoll详解
https://blog.csdn.net/coolgw2015/article/details/79719328 面试之多路复用
https://blog.csdn.net/yfkscu/article/details/38141635?locationNum=7 常见Linux IO模型分析