网络IO模型:BIO、NIO、AIO的区别
1.BIO,即Blocking IO,同步阻塞IO,最原始的实现方式,每个socket在进行IO请求时(发送数据或接收数据)都会阻塞线程,所以有多少个IO请求就需要多少个线程;
这里同步和异步是一种逻辑概念,比如我调用某个接口是异步接口,即对方不会等处理完业务后告诉我业务处理结果,而是直接就返回了,需要我们后续通过其他方式来验证是否执行成功
,但是这个IO本身又是同步的,即socket也是要同步把数据发送到了对方,也要同步获取到了对方返回的接收到了我们的请求的响应才算请求完成,因此这个层面它又是同步的;
而阻塞个人理解应该是要和线程进行挂钩,即阻塞会导致线程sleep、或者自旋或者wait等情况;
阻塞是分为这几种:
阻塞式发送:发送方线程会被阻塞(阻塞就是线程在这段时间无法做其他事情,哪怕它获取到了时间片)
非阻塞式发送:发送方线程send后立刻返回,然后可以执行其他操作
阻塞式接收:接收方线程调用receive方法后一直阻塞,直到有可用消息到达;(UDP就是要完整的报文,而TCP则看情况可能是一个字节也可能是多个字节)
非阻塞式接收:调用receive方法后要么得到一个有效的数据,要么直接返回一个空值而不会等待一个有效数据,即不会被阻塞;
异步是callback的主动通知?
这里的概念其实也和具体实现有关,不同语言,不同系统的这几个概念也不是完全一样的。。
java里的NIO是同步非阻塞,这里的同步也是api的同步(逻辑概念),即我调用read方法是同步检测是否读取到了数据,但是它没有读取到也会立刻返回;
BIO最大的问题就是一个线程只能处理一个连接,即连接无论是发送还是接收或者连接服务都会阻塞当前线程(IO阻塞,非SLEEP或wait);
导致连接一多需要的线程也越多,然后线程切换带来资源消耗;
NIO则是解决了一个线程只能处理一个连接的问题,这里就涉及到非阻塞读和非阻塞写:
非阻塞读:即线程socket.read(buffer)会由用户态转变为内核态,去内核中查询socket接收缓冲区是否有数据,如果缓冲区有数据则立刻拷贝到buffer里(用户态;这个拷贝过程是阻塞的),并在返回值里告诉读取了多少数据;如果缓冲区没有数据,则会立刻返回而不会产生阻塞,也不会让出CPU,只是返回-1表示没有读取到数据;【所以非阻塞读涉及到 内核态缓冲区,用户态buffer,读取的数据量这些概念】
非阻塞写:阻塞写是用户态将buffer的数据写入到内核态缓冲区(写的缓冲区,读的是另外一个【读写缓冲区就是一根管道,有source、target】),必须得把buffer的所有数据都写完才会返回,所以它需要等待操作系统将该socket内核态写缓冲区的数据发送成功清理出缓冲空间,然后用户态的buffer能完全写入到缓冲区才结束;而非阻塞写则是不要求将用户态的buffer全部写完,而是可以写多少就写多少,写完立刻返回用户写成功了多少,然后由用户自己去写剩下的数据;
【而之所以BIO的socket的IO操作都会消耗一个线程就是因为它的IO操作方法的头铁行为(注意不是说BIO一个socket就一定会消耗一个线程,是完全可以创建N个socket保存到全局数组里不做任何操作,然后不阻塞任何线程的,这里说的都是指它的IO操作方法会导致占用线程,即阻塞线程);而我们现在有了非阻塞读和非阻塞写方法,那么socket的IO操作方法就不会因为没有完全成功(如没有完全写入数据成功或没有读取到任何数据)而阻塞线程;我们就可以通过一个线程来轮询检查多个socket的缓冲区是否有数据到达,比如发现socket a的读缓冲区有数据了/写缓冲区还有空间 则立刻返回数据或可写空间大小(当然没有数据或写缓冲空间也会立刻返回,只是返回告诉用户态没有数据或可写空间而已),在java里就是一个线程不断的执行select()方法,然后判断其是否可读,如果可读则得到可读的channel(就是服务端可以和多个客户端通信,每个通信都会产生一个channel,在同步阻塞里每个channel是单独进行BIO读写方法的,所以一个服务socket连接的客户端越多且都进行通信就会导致被阻塞的线程越多),然后用这个channel.read(buffer)来读取这个channel的读缓冲区里的数据到用户态;所以Java的NIO似乎只适合于服务端,客户端用处不大,因为它不存在多路复用,它自己就只会产生一根管道,除非一个NIO客户端可以连接多个服务端产生多个channel或者多个socketChannel对象可以共用一个selector检查这些channel的缓冲区(看了下还真的可以,先创建一个Selector对象,然后一个socketChannel.open和.connect服务a后,通过socketChannel.register(selector, XXOPER),然后又通过socketChannel2.open和.connect服务b后,再通过socketChannel2.register(selector, XXOPER)来实现一个selector监听同多个socketChannel对象的XX缓冲区】,NIO也有缺点,就是得有个单独的线程不断的select()检查多个channel(读/写/连接等的缓冲区),如果一直没有数据那反而成为累赘,CPU在空转。
基于epoll技术的NIO的select是一个阻塞方法!!!(如果不是的话它就是一个不断循环不阻塞的方法【或者内部有个类似Thread.sleep(1)的操作】),它如果没有检测到监听的事件会阻塞当前线程
并让出CPU,然后epoll发现有selector要监听的事件会通知selector,从而唤醒对应的线程并且select()方法返回(所以基于epoll技术的NIO在应用侧的效率是已经很高了)
【但是早期的NIO,即2002年的时候,那时候还没有epoll,这个时候select是一直占用CPU的】
Netty也是基于NIO技术,只不过用起来更方便好用;
AIO就是增加Future,当read的时候会直接返回Future<Integer>对象,通过future.get()来获取真正读取的值,但是这种方式和BIO没啥区别,也是会阻塞当前线程(Java目前没有协程,所以阻塞当前方法就等价于阻塞当前线程,建议用回调的方式,read是给出回调方法,有数据了则通过回调方法来处理获取数据后的业务逻辑(这个时候底层可能也用到了类似Selector不断监听管道是否有数据,有则获取到用户态并通过该回调方法通知用户;查了下似乎AIO在用户代码层面没有用到多路复用,它只是写法优雅一点,当数据传输一直长期存在时性能远不及NIO【数据传输一直存在则CPU就不会长期空转】【但是查了一下似乎AIO用了操作系统提供的多路复用回调支持?不管怎么样说,get肯定是BIO的,回调可能有用多路复用(查了gpt说是通过epoll来实现的多路复用;而NIO的多路复用可以不依赖epoll,只需要操作系统提供相关的检测方法即可)】);
AIO模型则是通过操作系统的异步机制实现的,将I/O操作的完成通知交给操作系统处理,应用程序将I/O请求提交给操作系统,然后操作系统会负责处理实际的I/O操作。当操作完成时,操作系统会通知应用程序,从而实现异步的I/O操作,而应用态的通知实现就是回调函数,即CompletionHandler接口,应用程序可以通过该接口注册回调方法,在操作完成时被调用。
AIO在linux里是通过epoll实现的;
BIO适合文件的读取,因为它的“读缓冲区”一直有数据,所以它的阻塞是必要的拷贝过程,写也一样,“写缓冲区”一直可写(因为数据会几乎立刻写到文件里,不像网络一样还需要经过漫长的网络和等待对方确认);
AIO的回调是通过线程池来调用的,可以自定义线程池;
posted on 2023-06-29 22:53 Silentdoer 阅读(37) 评论(0) 编辑 收藏 举报