网络IO模型:BIO、NIO、AIO的区别

发布时间 2023-06-29 23:03:40作者: Silentdoer

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在空转。

 

AIO就是增加Future,当read的时候会直接返回Future<Integer>对象,通过future.get()来获取真正读取的值,但是这种方式和BIO没啥区别,也是会阻塞当前线程(Java目前没有协程,所以阻塞当前方法就等价于阻塞当前线程,建议用回调的方式,read是给出回调方法,有数据了则通过回调方法来处理获取数据后的业务逻辑(这个时候底层也是用到来类似Selector);