了解Netty之基本理论篇--IO模型
也许迷途的惆怅,会扯碎我的脚步,可我相信未来会给我一双梦想的翅膀
虽然失败的苦痛,已让我遍体鳞伤,可我坚信光明就在远方
通常,关于IO模型听得比较多的,同步阻塞IO、同步非阻塞IO等等,然后前面又说到一个多路复用模型,还有Reactor、Proactor模型。。。。
名词概念多了以后,就有点傻傻分不清楚了
然后今天特地整理了一下思路,轻装上阵
在IO模型前,首先需要了解下计算机操作系统层面的一些操作,当应用程序进行读写操作(无论是基于Socket还是文件)时,应用程序底层都是Input和Output操作应用程序进程缓冲区的数据,操作完成后,操作系统的调度系统将进程缓冲区的数据读写到操作系统内核缓冲区,再由操作系统内核kernel完成读写到最终的网卡<网络通信>或者磁盘<文件读写>。当然这里也有例外,比如JavaNIO中内存映射文件操作是个例外,详情可见,文中关于通过内存映射文件完成文件复制的说明。
为何需要缓冲区?频繁的操作系统IO操作调用,会有很大的功耗,所以一般来说,程序需要减少IO操作,比如日常工作中经常说的“不要在循环体中访问数据库,会很卡”,为什么呢?数据库其实也是把数据保存到系统磁盘文件中的,所以数据库连接然后查询数据库的一系列操作,也属于IO操作。所以需要先将数据加载到操作系统内核缓冲区,同时操作系统调度将数据加载到程序进程缓冲区,方便减少对操作系统的IO操作。
即简单用图解释下,程序的IO操作涉及的2个步骤
然后这里要说的IO模型就是基于操作系统层面的读写,即上图的前半段操作
一般来说,IO模型包含5个:
同步阻塞IO、同步非阻塞IO、信号驱动IO、IO多路复用、异步非阻塞IO
下面依次简单了解这几个模型
同步以一个简单例子来描述这个过程,假定有个房东,有很多套房子需要去收租,然后每次收租需要收取现金,同时给租户开具收据,看过很多短视频,房东收房租喜欢拿个装钱的袋子,那这里也有个装钱的袋子。类比IO操作,房东是应用程序,收到的租金是需要读取的数据<read>,开具的收据是需要写出的数据<write>。
同步阻塞IO
房东,去某个租户家收租,等待租户清点现金给房东,房东接过现金,开具发票,对于房东来说,在开始去收租开始,需要一直等到租户把现金清点好,将现金放入袋子中。然后房东才把发票开具好,给到租户。
在租户给到现金之前,房东就这么傻傻的在门口等着。
这个过程,我们可以简单的理解为同步阻塞IO
这个装钱的袋子,当收到租金之后,肯定比之前更重了,于是袋子的状态和之前就不一样了,这里我们可以理解为文件描述符(FD)
在kernel数据没有复制到用户应用程序内存,程序线程就傻傻处于一个阻塞的状态,同时,由于读的时候不能写,写的时候不能读,读写之间又是一个同步状态,即为同步阻塞IO
这种模式也是比较常见的了,比如:,读写的同步操作,通常会用多线程,即伪异步来做!
同步非阻塞IO
当多次这样子收租方式之后,房东有点厌倦了,他不想在收租的时候,傻傻的等着租户清点现金完之后,然后收到钱,主要租金又高,租客数钱数半天,等着就很烦躁,于是房东想到一个办法,将收钱的袋子,放在门口<假定钱袋只能往里放钱,不可以拿走,然后人们素质又很高,不存在把钱袋子拿跑的情况>,然后房东可以在租客清点租金的期间内,该玩手机玩会手机,该干啥就干会儿啥。在这个期间,只要保证每隔一段时间,房东回过头来看钱袋子里有多了钱没有就好了。
直到租客交了租金,房东开了发票,带着钱开心的走了。这个过程结束了
每隔一段时间就回头看看,这个操作其实就是对文件描述符的一个轮询的过程,当数据准备就绪,则获取数据,否则就可以继续做其他的事,而不会阻塞在这里。
此种方式也是JavaNIO的模型中的一部分,但是此种模型也有不好的地方,即轮询对于CPU来说是比较大的浪费
信号驱动IO
房东对上述过程还是不满意,因为需要每隔一段时间来看看,于是他在钱袋上安装了一个开关,当有钱进入的时候,开关就会响,于是他就可以安安心心的玩手机看直播了(#^.^#)
即,在kernel给到信号前,就可以一心一意做其他的事了。
按照顺序,本来应该说IO多路复用,但这里先把最后一个异步非阻塞说了,最后来说IO多路复用
异步非阻塞IO
有一次,房东和他儿子和女儿一起去收租,于是按照上述过程,女儿收钱,然后儿子开收据,收钱和开收据可以同时做了<这里忽略如果先收到收据不给钱的租客的情况,大家信誉良好,都是会给钱的>,于是,房东就直接可以义无反顾的去做自己的事了
即只要应用程序主线程发起了读写请求之后,就什么就不管了,只需要在回调中处理自己的事就好,中比较清晰的介绍了这个过程。
异步非阻塞的IO,在操作系统层面,实现得并不多,尤其在Linux上,所以,目前在软件编程过程中是使用得比较少的。
IO多路复用
基于同步非阻塞IO的过程,房东认为虽然方法可行,但是一家一家的收太麻烦了,于是他固定的地方,放好很多的钱袋,对应每个租户,告知租客,在收租那天,租客自己将钱清点好之后,放到钱袋中,于是收租的过程效率更高了
此模型基本是基于同步非阻塞IO的一个升级版本,同时将多个IO操作请求封装于通道中,都注册到Select上,通过不停的循环Select中通道及其通道状态(即文件描述符的状态),做各自不一样的事
那么基于IO多路复用,目前流行的多路复用IO模型的主要实现有四种:Select、poll、epoll、kqueue
主要实现 |
性能 |
具体实现方案 |
支持操作系统 |
select |
较高 |
Reactor模型 |
Window/Linux |
poll |
较高 |
Reactor模型 | Linux |
epoll |
高 |
Reactor/Proactor | Linux |
kqueue |
高 |
Proactor | Linux |
试想这么几个问题:首先,租客不会一次性到来,会分批次的来,那么钱袋也是分批次的增加,房东轮询每个钱袋,也是越来越多,压力会显然增大,同时当发现某个钱袋装好了钱,于是自己便去处理这家人收租的事宜,于是轮询便停了下来,这样子是需要等到房东对于这家人的租金都处理妥当了<比如房东还需要检查钱是不是真的,有没有那么多等等事情,最后还要开具收据等一些列操作>之后,才能继续去轮询其他的钱袋了
这种方式其实对于程序来说,其实是不太理想的,于是,可以做个改进,房东就一直坚守着查看租客的增加,或者完成收租过程之后的租客减少的过程,当某个租客到来或者离开的时候,对应给到相应钱袋的状态描述符,同时将这个租客的接下来的操作交给儿子或者女儿去做,而房东就不在去做其他的事情了,
基于此,这里把房东一直坚守的过程称为一个反应堆(Reactor),把钱袋不断增加/减少的过程,视为一个Socket的新连接/断开连接,当有新的socket连接到来之后,注册对应事件,同时利用事件驱动机制<即事件到来的时候触发事件,而不是漫无目的的去监视事件>,当事件触发,即文件操作符的状态改变为某种状态的时候,由线程去处理相应的事件
这就是一个简易的Reactor模型,即Reactor模型是一个IO多路复用模型的实现方案
与此对应的Proactor模型,它则不在强调对应某种文件描述符而做相应的操作,相反,它所关心的是当前文件描述符是否是完成状态,类似房东收租,只关心钱袋是否已经有钱,只要有钱了,直接异步做完成收钱后所需要做的事,而不在意,当前钱袋是一个什么样的状态。
对比Reactor和Proactor两种模型,主要有以下区别:
1、Reactor实现同步I/O多路分发,Proactor实现异步I/O分发。
Reactor比较倾向于处理网络I/O,涉及文件I/O,单线程的Reactor可能被I/O阻塞导致事件分发异常。所以文件I/O最好还是使用Proactor模式,或者用多线程模拟实现异步I/O的方式。
2、 Reactor模式注册的是文件描述符的就绪事件,而Proactor模式注册的是完成事件。
即Reactor模式有事件发生的时候要判断是读事件还是写事件,然后用再调用系统调用(read/write等)将数据从内核中拷贝到用户数据区继续其他业务处理。Proactor模式一般使用的是操作系统的异步I/O接口,发起异步调用(用户提供数据缓冲区)之后操作系统将在内核态完成I/O并拷贝数据到用户提供的缓冲区中,完成事件到达之后,用户只需要实现自己后续的业务处理即可。
3、Reactor模式是一种被动的处理,即有事件发生时被动处理。而Proator模式则是主动发起异步调用,然后循环检测完成事件。
前面已经说到了,Netty是采用NIO,为什么不用AIO呢?
在这里相信已经有了答案
1、Netty整体架构是Reactor模型,而AIO是Proactor模型,当Netty使用AIO则会将两种模型混合在一起,比较混乱
2、在Linux系统上,AIO的底层实现,是利用多路复用IO中的epoll,没有很好的实现AIO,在性能上没有明显优势,同时Netty主要使用在Linux上
3、AIO接收数据前需要预分配缓存<因为异步操作>,所以当连接数非常大但流量小的时候,内存耗费比较严重
以上就是对于IO基本理论的一些简单介绍,可能存在不正确的地方,请多多指正
下一节,将正式进入Netty,介绍使用Netty如果处理数据传输中的粘包和半包情况
烦请动动手指,转发关注