服务器I/O模型

Java的I/O发展简史

在JDK1.4推出Java NIO之前,基于Java的所有Socket通信都采用了同步阻塞模式(BIO),这种一请求一应答的通信模型简化了上层应用开发,但是在性能和可靠性方面却存在着巨大的瓶颈。因此,在很长一段时间里,大型的应用服务器都采用C或者C++语言开发,因为它们可以直接使用操作系统提供的异步I/O或者AIO能力。当并发访问量增大、响应时间延迟增大之后,采用Java BIO开发的服务端软件只有通过硬件的不断扩容来满足高并发和低延迟,它地增加了企业的成本,并且随着集群规模的不断膨胀,系统的可维护性也面临巨大的挑战,只能通过采购性能更高的硬件服务器来解决问题,这会导致恶性循环。

正是由于Java传统BIO的拙劣表现,才使用Java支持非阻塞I/O的呼声日渐高涨,最终JDK1.4版本提供了新的NIO类库,Java终于也可以支持非阻塞I/O了。

I/O模型

我们从I/O的阻塞非阻塞、I/O处理的单线程多线程角度探讨服务器模型。对于I/O,可以分成阻塞I/O非阻塞I/O两大类型。阻塞I/O在做I/O读写操作时会使当前线程进入阻塞状态,而非阻塞I/O则不进入阻塞状态。对于线程,单线程情况下由一条线程负责所有客户端连接的I/O操作,而多线程情况下则由若干线程共同处理所有客户端连接的I/O操作。下面将对线程和(非)阻塞组合成的模型进行分析,看看各种服务器模型有哪些不同,各自的优缺点又有哪些。

单线程阻塞I/O模型

单线程阻塞I/O模型是最简单的一种服务器模型,几乎所有程序员在刚开始接触网络编程时都从这个简单的模型开始。这种模型只能同时处理一个客户端访问,并且在I/O操作上是阻塞的,线程会一直在等待,而不会做其他事情。对于多个客户端访问,必须要等到前一个客户端访问结束才能进行下一个访问的处理,请求一个一个排队,只提供一问一答服务。下图展示了同步阻塞服务器响应客户端访问的时间节点图。
在这里插入图片描述

这种模型的特点在于单线程和阻塞I/O。单线程即服务器端只有一个线程处理客户端的所有请求,客户端连接与服务器端的处理线程比是n:1,它无法同时处理多个连接,只能串行处理连接。而阻塞I/O是指服务器在读写数据时是阻塞的,读取客户端数据时要等待客户端发送数据并且把操作系统内核数据复制到用户进程中,这时才解除阻塞状态。写数据回客户端时要等待用户进程将数据写入内核并发送到客户端后才解除阻塞状态。这种阻塞给网络编程带来了一个问题,服务器必须要等到客户端成功接收才能继续往下处理另外一个客户端的请求,在些期间线程将无法响应任何客户端请求。

该模型的特点:它是最简单的服务器模型,整个运行过程都只有一个线程,只能支持同时处理一个客户端的请求(如果有多个客户端访问,就必须排队等待),服务器系统资源消耗较小,但并发能力低,容错能力差。

多线程阻塞I/O模型

针对单线程阻塞I/O模型的缺点,我们可以使用多线程对其进行改进,使之能并发地对多个客户端同时进行响应。多线程模型的核心就是利用多线程机制为每个客户端分配一个线程。

如下图所示,服务器开始监听客户端的访问,假如有两个客户端发送请求过来,服务器端在接收到客户端请求后分别创建两个线程对它们进行处理,每条线程负责一个客户端连接,直到响应完成。期间两个线程并发地为各自对应的客户端处理请求,包括读取客户端数据、处理客户端数据、写数据回客户端等操作。
在这里插入图片描述

这种模型的I/O操作也是阻塞的,因为每个线程执行到读取或写入操作时都将进入阻塞状态,直到读取到客户端的数据或数据成功写入客户端后才解除阻塞状态。尽管I/O操作阻塞,但这种模式比单线程处理的性能明显高了,它不用等到第一个请求处理完才处理第二个,而是并发地处理客户端请求,客户端连接与服务端处理线程的比例是1:1。

多线程阻塞I/O模型的特点:支持对多个客户端并发响应,处理能力得到大幅提高,有较大的并发量,但服务器系统资源消耗量较大,而且多线程之间会产生线程切换成本,同时拥有较复杂的结构。

单线程非阻塞I/O模型

多线程阻塞I/O模型通过引入多线程确实提高了服务端的并发处理能力,但每个连接都需要一个线程负责I/O操作。当连接数量较多时可能导致机器线程数量太多,而这些线程大多数时间却处理等待状态,造成极大的资源浪费。鉴于多线程阻塞I/O模型的缺点,有没有可能用一个线程就可以维护多个客户端连接并且不会阻塞在读写操作呢?答案是有的,就是单线程非阻塞I/O模型。

单线程非阻塞I/O模型最重要的一个特点是,在调用读取或写入接口后立即返回,而不会进入阻塞状态。在探讨单线程非阻塞I/O模型前必须要先了解非阻塞情况下套接字事件的检测机制,因为对于单线程非阻塞模型最重要的事件是检测哪些连接有感兴趣的事件发生(包括可读事件、可写事件),一般会有如下三种检测方式。

非阻塞情况下套接字事件的检测机制

  • (1)应用程序遍历套接字的事件检测

当多个客户端向服务器请求时,服务器端会保存一个套接字连接列表中,应用层线程对套接字列表轮询尝试读取或写入。对于读取操作,如是成功读取到若干数据,则对读取到的数据进行处理;如果读取失败,则下一个循环再继续尝试。对于写入操作,先尝试将数据写入指定的某个套接字,写入失败则下一个循环再继续尝试。
在这里插入图片描述

这样看来,不管有多少个套接字连接,它们都可以被一个线程管理,一个线程负责遍历这些套接字列表,不断地尝试读取或写入数据。这很好地利用了阻塞的时间,处理能力得到提升。但这种模型需要在应用程序中遍历所有的套接字列表,同时需要处理数据的拼接,连接空闲时可能也会占用较多资源(不停的循环遍历),不适合实际使用。对些改进的方法是使用事件驱动的非阻塞方式。
由应用程序遍历套按字尝试读取与写入。需要处理读取到的数据的拼接,因为每次读取到的数据都可能只是一小部分。

  • (2)内核遍历套接字的事件检测

这种方式将套接字的遍历工作交给了操作系统内核,把对套接字遍历的结果组织成一系列的事件并返回应用层处理。对于应用层,它们需要处理的对象就是这些事件。这就是其中一种事件驱动的非阻塞方式的实现

如下图所示,服务器端有多个客户端连接,应用层向内核请求读写事件列表。内核遍历所有套接字并生成对应的可读列表readList和可写列表writeList。readList标明了每个套接字是否可读,例如socket1的值是1,表示可读,socket2的值为0,表示不可读。wirteList则标明了每个套接字是否可写。应用层遍历读写事件列表readList和writeList,做相应的读写操作。
在这里插入图片描述

内核遍历套接字时已经不用在应用层对所有套接字进行遍历且尝试读写了,将遍历工作下移到内核层,这种方式有助于提高检测效率。然而,它需要将所有连接的可读事件列表和可写事件列表传到应用层,假如套接字连接数量大,列表从内核复制到应用层也是不小的开销。另外,当活跃连接较少时,内核与应用层之前存在很多无效的数据副本,因为它将活跃和不活跃的连接状态都复制到应用层中。

  • (3)内核基于回调的事件检测

    通过遍历的方式检测套接字是否可读可写是一种效率比较低的方式,不管是在应用层中遍历还是在内核中遍历。所以需要另外一种机制来优化遍历的方式,那就是回调函数。内核中的套接字都对应一个回调函数,当客户端往套接字发送数据时,内核从网卡接收数据后就会调用回调函数,在回调函数中维护事件列表,应用层获取此事件列表即可得到所有感兴趣的事件(包含可读,可写事件)。

    内核基于回调的事件检测包含两种方式:基于回调机制的完全套接字可读可写列表内核基于回调的事件检测方式

    基于回调机制的完全套接字可读可写列表是用可读列表readList和可写列表wirteList标记读写事件,套接字的数量与readList和writeList两个列表的长度一样,readList第一个元素标为1则表示套接字1可读,同理writeList第二个元素标为1则表示套接字2可写。

如下图所示,多个客户端连接服务器端,当客户端发送数据过来时,内核从网卡复制数据成功调用回调函数将readList第一个元素置为1,应用层发送请求读、写事件列表,返回内核包含了事件标识的readList和writeList事件列表,进而分别遍历读事件列表readList和写事件列表writeList,对置为1的元素对应的套接字进行读或写操作。这样就避免了遍历套接字的操作,但仍然有大量无用的数据(状态为0的元素)从内核复制到应用层中。于是就有了第二种事件检测方式(内核基于回调的事件检测方式)。
在这里插入图片描述

内核基于回调的事件检测方式:服务端有多个客户端套按字连接。首先,应用层告诉内核每个套接字感兴趣的事件(读写事件)。接着,当客户端发送数据过来时,对应会有一个回调函数,内核从网上复制数据成功后即调用函数将套接字1作为可读事件event1加入到事件列表。同样地 ,内核发现网卡可写时就将套接字2作为可写事件event2添加到事件列表中。最后,应用层向内核读、写事件列表,内核将包含了event1和event2的事件列表返回应用层,应用层通过遍历事件列表得知套接字1有数据待读取,于是进行读操作,而套接字2则可以写入数据。
在这里插入图片描述

上面两种方式由操作系统内核维护客户端的所有连接并通过回调函数不断更新事件列表,而应用层线程只要遍历这些事件列表即可知道可读取或可写入的连接,进而对这些连接进行读写操作,极大提高 了检测效率,自然处理能力也更强了。

对于Java来说,非阻塞I/O的实现完全是基于操作系统内核的非阻塞I/O,它将操作系统的非阻塞I/O的差异屏蔽并提供统一的API,让我们不必关心操作系统。JDK会帮我们选择非阻塞I/O的实现方式,例如对于Linux系统,在支持epool的情况下JDK会优先选择epool实现Java的非阻塞I/O.这种非阻塞方式的事件检测机制就是效率最高的“内核基于回调的事件检测”中的第二种试。

多线程非阻塞I/O模型

  • 最朴实、最自然的做法就是将客户端连接按组分配给若干线程,每个线程负责处理对应组内的连接。假如有4个客户端访问服务器,服务端将套接字1和套接字2交由线程1管理,而线程2则管理套接字3和套接字4,通过事件检测及非阻塞读写就可以让每个线程都高效处理。
    在这里插入图片描述

  • 最经典的多线程非阻塞I/O模型方式是Reactor模式。

-------------------摘自汪建的《Tomcat内核设计剖析》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值