上一篇主要讲述了从阻塞IO到非阻塞IO再到IO多路复用演变的过程中所面临的的问题,最终引出了Netty,Netty是在一种IO模型(IO多路复用)的基础上实现了Reactor模式,在此模式的基础上Netty才能大放异彩,因此如果想深入理解Netty,IO模型的工作原理必须要了解。今天我们深入说一说IO模型。
IO模型
IO模型有五种:BIO、NIO、IO多路复用、信号驱动IO、AIO。前四种都是同步IO,只有AIO才是真正的异步IO。
我们不能说哪种IO模型是最好的,只能说哪种场景更适合哪种IO模型。
我们先看下边的五张图:(图片来源:《Netty4核心原理与手写RPC框架实战》)
BIO模型
BIO在读取数据(recvfom)的时候,如果数据没有准备好,用户线程就会阻塞,这期间用户线程什么也干不了只能挂起,直到内核将读取的数据准备好,拷贝至用户内存,唤醒线程返回读取结果。
BIO由于它的阻塞机制,导致它天然的绑定线程,而服务端的线程资源是有限的,在处理并发场景的时候会消耗大量的服务器资源从而导致客户端大量的连接被拒绝,但是它就一无是处了吗?其实不然,就像上面说的,如果在并发情况不高的场景中,BIO反而会表现的更好,而且BIO在编程上又非常简单,所以何乐而不为呢。
NIO模型
从图中我们可以看出NIO没有阻塞点,也就是它在读取数据(recvfom)的时候不阻塞,它是非阻塞IO,但是它的弊端就是可能存在无效的轮询。比如在数据包没有准备好的时候线程也在轮询,这无疑是浪费CPU资源的。
这里需要注意一点,非阻塞IO就意味着不阻塞吗?其实不是的,当数据包准备好的时候,此时读取数据(recvfom)就会和BIO一样,也需要阻塞,等待数据拷贝到用户内存,然后唤醒线程返回结果。
它和BIO的区别就是,当数据包没有准备好的时候,线程无需阻塞,可以继续做其他的事情,当数据包准备好的时候线程阻塞直到返回读取结果。
那么它能解决高并发场景吗?答案是不能,当同一时刻大量客户端连接的发送的数据包都准备好的时候,服务端的处理机制和BIO没有区别,服务端线程同一时刻只能处理一个客户端连接的数据包,这时候又来了大量的客户端连接请求,服务端线程由于正在处理已经准备好数据包的客户端连接而不能及时处理新发起的大量的客户端连接请求而导致连接被拒绝。
IO多路复用模型
IO多路复用是为了解决在NIO中的线程无效轮询和一个线程只能处理一个连接的问题。
增加一个Selector组件,selector组件绑定一个事件处理线程,该线程用于处理客户端连接。当客户端发起连接的时候会唤醒当前事件处理线程,且没有客户端发起连接时,当前事件处理线程会阻塞至Selector的select()方法,充分做到了线程"按需使用"。
当大量的客户端连接发起的时候,Selector绑定的事件线程如果不能及时处理也无所谓。大量的客户端连接可以先在Selector中注册,当然Selector中也不能无限制的积压客户端连接,其内部维护了一个队列,超过了这个队列限制,依然会出现连接拒绝的情况,但是相比于BIO和NIO,IO多路复用无疑是增加了"缓存"连接的能力,待事件线程处理完上一个连接后再从Selector维护的队列中获取下一个连接继续处理。
通常事件线程处理连接的速度是很快的,所以根据你的场景适当的调节队列的大小,就可以避免连接积压的问题。因此,IO多路复用是专门为高并发场景而生的 (上一篇我们说了在高并发场景的时候即便使用IO多路复用机制,如果我们使用的方式不当,依然会遇到瓶颈,因为一个Selector只能绑定一个线程,但是如果我们使用多个Selector一起去处理连接,问题就迎刃而解了,事实上,Netty就是这么做的) 。
如果在并发不高的场景,其表现反而不如BIO,因为IO多路复用有两个阻塞点,select()和read(),而BIO只有一个阻塞点read(),所以在并发很低的场景,IO多路复用并没有优势。
信号驱动IO模型
信号驱动IO是指进程预先告知内核,向内核注册一个信号处理函数,然后用户进程返回不阻塞,当内核数据就绪时会发送一个信号给进程,用户进程便在信号处理函数中调用IO读取数据,由于此IO模型应用场景较少,所以这里不介绍了。
异步IO模型
异步IO的工作机制:告知内核启动某个操作,并让内核在整个操作完成后通知用户线程。
这种模型与信号驱动IO模型的区别在于,信号驱动IO模型是由内核通知用户线程何时可以去读取数据,是线程主动的获取数据。这就意味着当内核将数据包准备好后会通知用户线程,用户线程就会主动发起内核调用,此时用户线程挂起,内核将数据拷贝至用户内存,然后唤醒用户线程返回结果,这个主动获取结果的过程和BIO没有区别。
而异步IO模型由内核告知我们IO操作何时已经完成,也就是说当数据包准备好后不需要用户线程主动从内核获取,而且内核已经把结果拷贝至用户内存中了,用户线程可以从用户内存中直接拿到结果。
以上就是五种IO模型的工作原理。
总结
我们知道Netty使用的是IO多路复用,也就是NIO+Selector,为何Netty没有使用AIO?AIO是真正意义上的异步IO,性能上也最优异,这是因为低版本linux并没有很好的支持这个模型,而且新版本即便是支持了,但是优化后所带来的性能“效益”微弱,这也是netty5(是基于AIO实现的)放弃的原因。