1、BIO(Blocking IO):

BIO (Blocking I/O) 是同步阻塞I/O模式,数据的读取写入必须阻塞在一个线程内等待其完成。

采用 BIO 通信模型 的服务端,通常由一个独立的Server(主线程)负责监听客户端的连接。一般是在 while(true) 循环中,服务端调用 accept() 方法,等待接收客户端的连接的监听请求,服务端一旦接收到一个连接请求,就可以建立通信套接字,并通过在这个通信套接字上进行读写操作,此时不能再接收其他客户端连接请求,只能等待同当前连接客户端的操作执行完成, 不过可以通过多线程来支持多个客户端的连接,如上图所示(每一个客户端连接请求都创建一个线程来单独处理)。

如果要让 BIO 通信模型 可以同时处理多个客户端请求,就必须使用多线程(主要原因是 socket.accept()socket.read()socket.write() 涉及的三个主要函数都是同步阻塞的),也就是说它在接收到客户端连接请求之后,为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。这就是典型的 一请求一应答通信模型 。我们可以设想一下,如果这个连接不做任何事情的话,不就是会造成不必要的线程开销,这是可以通过 线程池机制 来改善的,线程池还可以让线程的创建和回收成本相对较低。使用 FixedThreadPool 可以有效控制线程的最大数量,保证了系统有限资源的控制,实现了 N(客户端请求数量):M(处理客户端请求的线程数量)的伪异步 I/O 模型(N 可以远远大于 M)。

再设想一下,当客户端并发访问量增加后,这种模型又会出现什么问题?

在 Java 虚拟机中,线程是宝贵的资源,创建和销毁成本都很高,除此之外,线程的切换成本也是很高的。尤其在 Linux 这样的操作系统中,线程本质上就是一个进程,创建和销毁线程都是重量级的系统函数。如果并发访问量增加,会导致线程数急剧膨胀,可能会导致线程堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务。

小结:

多线程BIO服务器虽然解决了单线程BIO无法处理并发的弱点,但是也带来一个问题:如果有大量的请求连接到我们的服务器上,但是却不发送消息,那么我们的服务器也会为这些不发送消息的请求创建一个单独的线程,那么如果连接数少还好,连接数一多就会对服务端造成极大的压力。所以如果这种不活跃的线程比较多,我们应该采取单线程的一个解决方案,但是单线程又无法处理并发,这就陷入了一种很矛盾的状态,于是就有了NIO。

2、NIO(Non-Blocking IO)

同步非阻塞式IO,关键是采用了事件驱动的思想来实现了一个多路转换器。 NIO与BIO最大的区别就是只需要开启一个线程就可以处理来自多个客户端的IO事件,这是怎么做到的呢? 就是多路复用器,可以监听来自多个客户端的IO事件: A. 若服务端监听到客户端连接请求,便为其建立通信套接字(java中就是通道),然后返回继续监听,若同时有多个客户端连接请求到来也可以全部收到,依次为它们都建立通信套接字。 B. 若服务端监听到来自已经创建了通信套接字的客户端发送来的数据,就会调用对应接口处理接收到的数据,若同时有多个客户端发来数据也可以依次进行处理。 

                                                                     NIO Reactor多线程模型

其中Selector(选择器)的作用是循环监听多个客户端连接通道,如果通道中没有数据即客户端没有请求时它可以去处理别的通道或者做其他的事情,如果通道中有数据他就会选择这个通道然后进行处理,这就做到了一个线程处理多个连接。

总之就是在一个线程中就可以调用多路复用接口(java中是select)阻塞同时监听来自多个客户端的IO请求,一旦有收到IO请求就调用对应函数处理。

注意:上图为NIO Reactor多线程模型,详细解释见下文。

2.1 NIO三种模型

NIO主要包含三种线程模型:

  1. Reactor单线程模型

  2. Reactor多线程模型

  3. 主从Reactor多线程模型

2.1.1 REACTOR单线程模型:

这是一种较为极端的情况,单个线程完成所有事情,包括接收客户端的TCP连接请求,读取和写入套接字数据等。

对于一些小容量应用场景,可以使用单线程模型。但是对于高负载、大并发的应用却不合适,主要原因如下:

  1. 一个NIO线程同时处理成百上千的链路,性能上无法支撑,即便NIO线程的CPU负荷达到100%,也无法满足海量消息的编码、解码、读取和发送;

  2. 当NIO线程负载过重之后,处理速度将变慢,这会导致大量客户端连接超时,超时之后往往会进行重发,这更加重了NIO线程的负载,最终会导致大量消息积压和处理超时,NIO线程会成为系统的性能瓶颈;

  3. 可靠性问题:一旦NIO线程意外跑飞,或者进入死循环,会导致整个系统通信模块不可用,不能接收和处理外部消息,造成节点故障。

为了解决这些问题,演进出了Reactor多线程模型。

2.1.2 REACTOR多线程模型:

Reactor多线程模型与单线程模型最大的区别就是有一组NIO线程处理真实的IO操作。

Reactor多线程模型的特点:

  1. 有专门一个NIO线程-Acceptor线程用于监听服务端,接收客户端的TCP连接请求;

  2. 网络IO操作-读、写等由一个NIO线程池负责,线程池可以采用标准的JDK线程池实现(Java语言),它包含一个任务队列和N个可用的线程,由这些NIO线程负责消息的读取、解码、编码和发送;

  3. 1个NIO线程可以同时处理N条链路,但是1个链路只对应1个NIO线程,防止发生并发操作问题。

在绝大多数场景下,Reactor多线程模型都可以满足性能需求;但是,在极特殊应用场景中,一个NIO线程负责监听和处理所有的客户端连接可能会存在性能问题。例如百万客户端并发连接,或者服务端需要对客户端的握手消息进行安全认证,认证本身非常损耗性能。在这类场景下,单独一个Acceptor线程可能会存在性能不足问题,为了解决性能问题,产生了第三种Reactor线程模型-主从Reactor多线程模型。

2.2.3 主从REACTOR多线程模型:

主从Reactor线程模型与Reactor多线程模型的最大区别就是有一组NIO线程处理连接、读写事件。

主从Reactor线程模型的特点是:服务端用于接收客户端连接的不再是个1个单独的NIO线程,而是一个独立的NIO线程池。Acceptor接收到客户端TCP连接请求处理完成后(可能包含接入认证等),将新创建的SocketChannel注册到IO线程池(sub reactor线程池)的某个IO线程上,由它负责SocketChannel的读写和编解码工作。Acceptor线程池仅仅只用于客户端的登陆、握手和安全认证,一旦链路建立成功,就将链路注册到后端subReactor线程池的IO线程上,由IO线程负责后续的IO操作。

即从多线程模型中由一个线程来监听连接事件和数据读写事件,拆分为一个线程监听连接事件,线程池的多个线程监听已经建立连接的套接字的数据读写事件,另外和多线程模型一样有专门的线程池处理真正的IO操作。

 小结:

  • Reactor单线程模型:一个线程即监听连接事件、读写事件、完成数据读写(一般是调用对应函数/接口完成数据读写)。

  • Reactor多线程模型:即从单线程中由一个线程即监听连接事件、读写事件、完成数据读写,拆分为由一个线程专门监听各种事件(监听连接、读写事件),再由专门的线程池负责处理真正的IO数据读写。

  • 主从Reactor多线程模型:有一组NIO线程处理连接、读写事件,再由专门的线程池负责处理真正的IO数据读写。

2.2 I/O多路复用

相较BIO,如果将套接字读操作换成非阻塞的,那么只需要一个线程就可以同时处理套接字,每次检查一个套接字,有数据则读取,没有则检查下一个(程序上实现轮询方式),因为是非阻塞的,所以执行read操作时若没有数据准备好则立即返回,不会发生阻塞。

这种轮询的方式缺点是浪费CPU资源,大部分时间可能都是无数据可读的,不必仍不间断的反复执行read操作,I/O多路复用(IOmultiplexing)是一种更好的方法,在Linux世界中有这样三种机制可以用来进行I/O多路复用:

3、BIO和NIO的区别及场景

3.1 BIO和NIO的区别

BIO以流的方式处理数据,NIO以块的方式处理数据,块IO的效率比流IO高很多。(比如说流IO他是一个流,你必须时刻去接着他,不然一些流就会丢失造成数据丢失,所以处理这个请求的线程就阻塞了他无法去处理别的请求,他必须时刻盯着这个请求防止数据丢失。而块IO就不一样了,线程可以等他的数据全部写入到缓冲区中形成一个数据块然后再去处理他,在这期间该线程可以去处理其他请求)
BIO是阻塞的,NIO是非阻塞的。
BIO基于字节流和字符流进行操作的,而NIO基于Channel(通道)和Buffer(缓冲区)进行操作的,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。Selector(选择器)用于监听多个通道事件,因此使用单个线程就可以监听多个客户端通道。

3.2 各自应用场景

到这里你也许已经发现,一旦有请求到来(不管是几个同时到还是只有一个到),都会调用对应IO处理函数处理,所以:

(1)NIO适合处理连接数目特别多,比如10000个连接以上,并且每个客户端并不会频繁地发送太多数据。,Jetty,Mina,ZooKeeper等都是基于java nio实现。

(2)BIO方式适用于连接数目比较小且固定的场景,这种方式对服务器资源要求比较高,并发局限于应用中。

4、总结

  • 不管是BIO还是NIO都会为每个客户端请求建立通信套接字,一个套接字在服务端对应一个fd(文件描述符)。
  • NIO是可以做到用一个线程处理多个操作的。假设有10000个请求过来,根据实际情况,可以分配50或100个线程来处理。不像BIO一样需要分配10000个线程来处理。
  • 基本可以认为 “NIO = 非阻塞式I/O + I/O多路复用 ”。

参考:传统 BIO (Blocking I/O)

参考:Java BIO和NIO

参考:关于BIO和NIO的理解