来看下面这个图,当客户端发起一次Http请求时,服务端的处理流程时怎么样的?
简单来说可以分为以下几个步骤:
基于TCP协议建立网络通信。
开始向服务端端传输数据。
服务端接受到数据进行解析,开始处理本次请求逻辑。
服务端处理完成后返回结果给客户端。
在这个过程中,会涉及到网络IO通信,在传统的BIO模式下,客户端向服务端发起一个数据读取请求,客户端在收到服务端返回数据之前,一直处于阻塞状态,直到服务端返回数据后完成本次会话。这个过程就叫同步阻塞IO,在BIO模型中如果想实现异步操作,就只能使用多线程模型,也就是一个请求对应一个线程,这样就能够避免服务端的链接被一个客户端占用导致连接数无法提高。
同步阻塞IO主要体现在两个阻塞点
服务端接收客户端连接时的阻塞。
客户端和服务端的IO通信时,数据未就绪的情况下的阻塞。
在这种传统BIO模式下,会造成一个非常严重的问题,如下图所示,如果同一时刻有N个客户端发起请求,按照BIO模型的特点,服务端在同一时刻只能处理一个请求。将导致客户端请求需要排队处理,带来的影响是,用户在等待一次请求处理返回的时间非常长。意味着服务端没有并发处理能力,这显然不合适。
那么,服务端应该如何优化呢?
非阻塞IO从前面的分析发现,服务端在处理一次请求时,会处于阻塞状态无法处理后续请求,那是否能够让被阻塞的地方优化成不阻塞呢?于是就有了非阻塞IO(NIO)
非阻塞IO,就是客户端向服务端发起请求时,如果服务端的数据未就绪的情况下,客户端请求不会被阻塞,而是直接返回。但是有可能服务端的数据还未准备好的时候,客户端收到的返回是一个空的,那客户端怎么拿到最终的数据呢?
如图所示,客户端只能通过轮询的方式来获得请求结果。NIO相比BIO来说,少了阻塞的过程在性能和连接数上都会有明显提高。
NIO仍然有一个弊端,就是轮询过程中会有很多空轮询,而这个轮询会存在大量的系统调用(发起内核指令从网卡缓冲区中加载数据,用户空间到内核空间的切换),随着连接数量的增加,会导致性能问题。
多路复用机制I/O多路复用的本质是通过一种机制(系统内核缓冲I/O数据),让单个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是读就绪或写就绪),能够通知程序进行相应的读写操作
什么是fd:在linux中,内核把所有的外部设备都当成是一个文件来操作,对一个文件的读写会调用内核提供的系统命令,返回一个fd(文件描述符)。而对于一个socket的读写也会有相应的文件描述符,成为socketfd。
常见的IO多路复用方式有,都是LinuxAPI提供的IO复用方式,那么接下来重点讲一下select、和epoll这两个模型
select:进程可以通过把一个或者多个fd传递给select系统调用,进程会阻塞在select操作上,这样select可以帮我们检测多个fd是否处于就绪状态,这个模式有两个缺点
由于他能够同时监听多个文件描述符,假如说有个,这个时候如果其中一个fd处于就绪状态了,那么当前进程需要线性轮询所有的fd,也就是监听的fd越多,性能开销越大。
同时,select在单个进程中能打开的fd是有限制的,默认是,对于那些需要支持单机上万的TCP连接来说确实有点少
epoll:linux还提供了epoll的系统调用,epoll是基于事件驱动方式来代替顺序扫描,因此性能相对来说更高,主要原理是,当被监听的fd中,有fd就绪时,会告知当前进程具体哪一个fd就绪,那么当前进程只需要去从指定的fd上读取数据即可,另外,epoll所能支持的fd上线是操作系统的最大文件句柄,这个数字要远远大于
I/O多路复用的好处是可以通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。它的最大优势是系统开销小,并且不需要创建新的进程或者线程,降低了系统的资源开销,它的整体实现思想如图2-3所示。
客户端请求到服务端后,此时客户端在传输数据过程中,为了避免Server端在read客户端数据过程中阻塞,服务端会把该请求注册到Selector复路器上,服务端此时不需要等待,只需要启动一个线程,通过selector.select()阻塞轮询复路器上就绪的channel即可,也就是说,如果某个客户端连接数据传输完成,那么select()方法会返回就绪的channel,然后执行相关的处理即可。
异步IO异步IO和多路复用机制,最大的区别在于:当数据就绪后,客户端不需要发送内核指令从内核空间读取数据,而是系统会异步把这个数据直接拷贝到用户空间,应用程序只需要直接使用该数据即可。
图2-4异步IO在Java中,我们可以使用NIO的api来完成多路复用机制,实现伪异步IO。在网络通信演进模型分析这篇文章中演示了JavaAPI实现多路复用机制的代码,发现代码不仅仅繁琐,而且使用起来很麻烦。
所以Netty出现了,Netty的I/O模型是基于非阻塞IO实现的,底层依赖的是JDKNIO框架的多路复用器Selector来实现。
一个多路复用器Selector可以同时轮询多个Channel,采用epoll模式后,只需要一个线程负责Selector的轮询,就可以接入成千上万个客户端连接。
Reactor模型