IO漫谈

几种IO模型

在这里插入图片描述
出去补偿用的信号驱动的,我们可以将其他四种画成这样
在这里插入图片描述

严格意义上说,IO复用模型应该是一种BIO的形式。所以上图并不严谨。

  • BIO(同步,blocking I/O)
    在这里插入图片描述

  • IO复用模型(BIO)的一种变形
    在这里插入图片描述
    经典的Reactor模式,Java中的Selector和Linux中的epoll都是这种模型。java的NIO实际上是New IO,不要和下面的NIO混淆。Epoll的出现解决了当时困扰互联网的C10K问题。

  • NIO(同步 Non-blocking I/O)
    在这里插入图片描述
    在这里插入图片描述
    实际上是用的最少的。就是反复轮训。一次没有数据就等一段时间再问一次。socket如果设成阻塞就是采用这种模式。

  • Asynchronous NIO(异步 Non-blocking I/O)

用户程序可以通过向内核发出I/O请求命令,不用等带I/O事件真正发生,可以继续做 另外的事情,等I/O操作完成,内核会通过函数回调或者信号机制通知用户进程。这样很大程度提高了系统吞吐量。经典的Proactor模式
在这里插入图片描述
简单来说,BIO里用户最关心“我要读”,NIO里用户最关心"我可以读了",在AIO模型里用户更需要关注的是“读完了”。阻塞和非阻塞的区别很明显,同步和非同步则是体现在内核和应用程序之间的配合。异步的情况下,内核处理数据和应用的工作是可以并行发生的。所以并不存在阻塞异步的情况(都阻塞了哪来什么并行发生)

操作系统与IO

https://blog.csdn.net/define_us/article/details/81568247
注意,有人也会把epoll叫做NIO,应该是概念不清原因。

JAVA与NIO

https://blog.csdn.net/define_us/article/details/79971640

两种经典模式

Proactor模式
  1. 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件,这是区别于Reactor的关键。

  2. 事件分离器等待读取操作完成事件。在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作(异步IO都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作,操作系统扮演了重要角色),并将读取的内容放入用户传递过来的缓存区中。这也是区别于Reactor的一点,Proactor中,应用程序需要传递缓存区。

  3. 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。可以直接对数据进行操作。

Proactor中写入操作和读取操作,只不过感兴趣的事件是写入完成事件。用户程序把缓冲区给了交给内核,内核读或写完成以后通知应用程序。

在这里插入图片描述
在这里插入图片描述

可以看到,上述过程严重依赖于操作系统对异步操作的支持。所以很遗憾,目前在linux上我们没有如此完善的机制(windows上倒是可以。)所以采用Proactor模式的框架一般是基于现有的epoll的封装。如boost.asio中,用户在发起async_read后,可以去进行其它操作,数据将会从内核buffer写入应用buffer,数据拷贝完毕会调用用户提供的回调函数。

Reactor模式

Reactor模式是一种公认的高性能IO模型。典型的使用案例是Netty,Redis等。
最原始的网络编程思路就是使用一个While循环,不断监听端口是否有套接字连接。

while(true){ 
	socket = accept(); 
	handle(socket) 
} 

显然这种方法过于悲剧,所以稍微聪明一点的人做了一定改进(Tomcat的早期版本)

while(true){ 
	socket = accept(); 
	new thread(socket); 
} 

。根据上面的思路,我们建立了我们经典的经典的BIO模式如下
在这里插入图片描述
① 服务器端的Server是一个线程,线程中执行一个死循环来阻塞的监听客户端的连接请求和通信。
② 当客户端向服务器端发送一个连接请求后,服务器端的Server会接受客户端的请求,ServerSocket.accept()从阻塞中返回,得到一个与客户端连接相对于的Socket。
③ 构建一个handler,将Socket传入该handler。创建一个线程并启动该线程,在线程中执行handler,这样与客户端的所有的通信以及数据处理都在该线程中执行。当该客户端和服务器端完成通信关闭连接后,线程就会被销毁。
④ 然后Server继续执行accept()操作等待新的连接请求。

  • 缺点在于线程需要的数目实在太多。连接数肯定是有上限的。另外,线程的反复创建销毁也需要代价(通过一个线程池来解决)。线程太多频繁切换存在成本。如果使用的是长连接,客户端和服务端的交互并不频繁,则对应的线程会一直存在。目前,这种模式适用于少量连接,大量数据交互的情况下。

单线程的Reactor模式如下

在这里插入图片描述

① 服务器端的Reactor是一个线程对象,该线程会启动事件循环,并使用Selector来实现IO的多路复用。注册一个Acceptor事件处理器到Reactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样Reactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。
② 客户端向服务器端发起一个连接请求,Reactor监听到了该ACCEPT事件的发生并将该ACCEPT事件派发给相应的Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将该连接所关注的READ事件以及对应的READ事件处理器注册到Reactor中,这样一来Reactor就会监听该连接的READ事件了。或者当你需要向客户端发送数据时,就向Reactor注册该连接的WRITE事件和其处理器。
③ 当Reactor监听到有读或者写事件发生时,将相关的事件派发给对应的处理器进行处理。比如,读处理器会通过SocketChannel的read()方法读取数据,此时read()操作可以直接读取到数据,而不会堵塞与等待可读的数据到来。
④ 每当处理完所有就绪的感兴趣的I/O事件后,Reactor线程会再次执行select()阻塞等待新的事件就绪并将其分派给对应处理器进行处理。

工作者线程池的改进版本

在这里插入图片描述
如上所示,把decode,compute,encode交给线程池做并配套人物队列。reactor完成read处理后会把任务转移到线程池的任务队列。线程池完成中的线程完成encode后,就向Reactor注册该连接的WRITE事件和其处理器。Reactor将执行对应的write处理。
但是,在负载很高时,上述模型却无法应付。这是因为,所有的I/O操作依旧由一个Reactor线程来完成,包括I/O的accept()、read()、write()以及connect()操作。

多Reactor线程模式

在这里插入图片描述
mainReactor负责监听连接,accept连接给subReactor处理,为什么要单独分一个Reactor来处理监听呢?因为像TCP这样需要经过3次握手才能建立连接,这个建立连接的过程也是要耗时间和资源的,单独分一个Reactor来处理,可以提高性能。

① 注册一个Acceptor事件处理器到mainReactor中,Acceptor事件处理器所关注的事件是ACCEPT事件,这样mainReactor会监听客户端向服务器端发起的连接请求事件(ACCEPT事件)。启动mainReactor的事件循环。

② 客户端向服务器端发起一个连接请求,mainReactor监听到了该ACCEPT事件并将该ACCEPT事件派发给Acceptor处理器来进行处理。Acceptor处理器通过accept()方法得到与这个客户端对应的连接(SocketChannel),然后将这个SocketChannel传递给subReactor线程池。

③ subReactor线程池分配一个subReactor线程给这个SocketChannel,也就是将SocketChannel关注的READ事件以及对应的READ事件处理器注册到subReactor线程中。当然你也注册WRITE事件以及WRITE事件处理器到subReactor线程中以完成I/O写操作。Reactor线程池中的每一Reactor线程都会有自己的Selector、线程和分发的循环逻辑。

④ 当有I/O事件就绪时,相关的subReactor就将事件派发给响应的处理器处理。注意,这里subReactor线程只负责完成I/O的read()操作,在读取到数据后将业务逻辑的处理放入到线程池中完成,若完成业务逻辑后需要返回数据给客户端,则相关的I/O的write操作还是会被提交回subReactor线程来完成。

注意,所以的I/O操作(包括,I/O的accept()、read()、write()以及connect()操作)依旧还是在Reactor线程(mainReactor线程 或 subReactor线程)中完成的。Thread Pool(线程池)仅用来处理非I/O操作的逻辑。

多Reactor线程模式将“接受客户端的连接请求”和“与该客户端的通信”分在了两个Reactor线程来完成。mainReactor完成接收客户端连接请求的操作,它不负责与客户端的通信,而是将建立好的连接转交给subReactor线程来完成与客户端的通信,这样一来就不会因为read()数据量太大而导致后面的客户端连接请求得不到即时处理的情况。并且多Reactor线程模式在海量的客户端并发请求的情况下,还可以通过实现subReactor线程池来将海量的连接分发给多个subReactor线程,在多核的操作系统中这能大大提升应用的负载和吞吐量。

一些案例

Netty 与 Reactor模式

Netty服务端使用了“多Reactor线程模式”
mainReactor ———— bossGroup(NioEventLoopGroup) 中的某个NioEventLoop
subReactor ———— workerGroup(NioEventLoopGroup) 中的某个NioEventLoop
acceptor ———— ServerBootstrapAcceptor
ThreadPool ———— 用户自定义线程池

流程:
① 当服务器程序启动时,会配置ChannelPipeline,ChannelPipeline中是一个ChannelHandler链,所有的事件发生时都会触发Channelhandler中的某个方法,这个事件会在ChannelPipeline中的ChannelHandler链里传播。然后,从bossGroup事件循环池中获取一个NioEventLoop来现实服务端程序绑定本地端口的操作,将对应的ServerSocketChannel注册到该NioEventLoop中的Selector上,并注册ACCEPT事件为ServerSocketChannel所感兴趣的事件。
② NioEventLoop事件循环启动,此时开始监听客户端的连接请求。
③ 当有客户端向服务器端发起连接请求时,NioEventLoop的事件循环监听到该ACCEPT事件,Netty底层会接收这个连接,通过accept()方法得到与这个客户端的连接(SocketChannel),然后触发ChannelRead事件(即,ChannelHandler中的channelRead方法会得到回调),该事件会在ChannelPipeline中的ChannelHandler链中执行、传播。
④ ServerBootstrapAcceptor的readChannel方法会该SocketChannel(客户端的连接)注册到workerGroup(NioEventLoopGroup) 中的某个NioEventLoop的Selector上,并注册READ事件为SocketChannel所感兴趣的事件。启动SocketChannel所在NioEventLoop的事件循环,接下来就可以开始客户端和服务器端的通信了。

Redis与Reactor模式

在redis中将感兴趣的事件及类型(读、写)通过IO多路复用程序注册到内核中并监听每个事件是否发生。当IO多路复用程序返回的时候,如果有事件发生,redis在封装IO多路复用程序时,将所有已经发生的事件及该事件的类型封装为aeFiredEvent类型,放到aeEventLoop的fired成员中,形成一个队列。通过这个队列,redis以有序、同步、每次一个套接字事件的方式向文件事件分派器传送套接字,并处理发生的文件事件。redis处理事件(无论是文件事件还是时间事件)都是以原子的方式进行的,中间不存在事件之间的抢占。这很容易理解,redis是单线程模型,不存在处理上的并发操作。

tomcat

NioEndpoint执行序列图
在这里插入图片描述

在这里插入图片描述

ngnix

总结

互联网编程中,经历了一阵子用户快速增长的剧痛。就是所谓的C10k问题。当时的服务器都是基于进程/线程模型的。每产生TCP连接,都会创建一个进程/线程。难道要创造1万个进程吗?当然,另一种思路是进行分布式。但是。如果我们一台服务器只能承受10k的并发连接,那么,维持一亿个用户就要一万个服务器。对于当时还是初创的QQ,Facebook无疑是一个巨大无比的成本。
人类注定绕不开提高单机连接数的难关。只有闷头干。早期的QQ显然选择规避,最早的QQ采用了UDP协议。

参考文献

https://www.cnblogs.com/winner-0715/p/8733787.html

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值