Netty指南(2)--- Java I/O入门

前言

本文我们将会介绍Java中的几种网络I/O模型,BIO、伪NIO、NIO、AIO,并提供演示代码。

BIO — 同步阻塞I/O

网络编程的本身是两个进程间的相互通信,其中server端提供位置信息(绑定ip及监听端口),客户端通过连接操作向服务器监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过套接字(socket)进行通信。

如上图所示,采用BIO通信模型的server端,由一个独立的Acceptor线程负责监听客户端连接,当没有客户端连接时服务端阻塞在accept操作上,在接收到客户端连接请求后为每一个客户端创建一个新的线程进行链路处理,处理完毕后通过输出流返回应答消息给客户端,线程销毁,这就是经典的一应一答通信模型。
该模型最大的问题是缺乏弹性伸缩能力,当客户端并发量增大时,服务端的线程个数和客户端的并发访问数呈1:1的关系,由于线程是Java虚拟机比较宝贵的系统资源,随着线程数的继续增大,程序会变得愈发的不稳定,最终会出现 栈溢出、创建新线程失败,进而导致宕机或僵死,无法对外提供服务。
点击查看BIO完整示例代码

伪异步IO — BIO的升级版

为了解决BIO中的问题,后端通过在BIO的基础上增加了一个线程池来处理多个客户端的接入,客户端和服务端的关系是 客户端个数M:线程池最大线程数N,由于线程池队列的关系,M可以远大于N,通过线程池可以灵活的调配线程资源,设置最大线程数以防止海量并发连接导致系统资源耗尽。

如上图所示,当有新的客户端接入时,将客户端的socket封装成task(该任务需实现java.lang.Runnable接口),由线程池中的空闲线程进行处理,由于线程池可以设置队列大小和最大线程数,所以这种I/O模型占用的系统资源是可控的,无论多少个客户端并发请求都不会导致资源耗尽和宕机。
不过要说的是 伪异步I/O 只是BIO一个简单的升级版,官方并没有这种叫法,它只是利用线程池解决了资源占用随着客户端连接数无限增长的问题,但是它无法解决BIO导致的线程阻塞问题。
比如:服务器处理缓慢,造成长时间阻塞,如果线程池中的可用线程都被阻塞,那后续所有I/O消息都将在队列中排队,由于线程池采用阻塞队列实现,队列积满后续入队列的操作会被阻塞,进而导致新的客户端被拒绝连接,客户端会发生大量连接超时,这就是一个典型的级联故障。
点击查看伪异步IO完整示例代码
由于伪异步IO,只是BIO的升级版,我称之为BIOPlus~~~,也因此示例代码直接在bio文件夹中创建了bioPlusServer文件夹,并增加了伪异步I/O的server端实现,客户端并没有改动。

NIO — 同步非阻塞I/O

首先需要澄清一个概念,NIO到底是什么简称?官方叫法是 NewIO,因为相对之前的I/O类库NIO是新增的,它的目标是让Java支持非阻塞I/O,所以也有很多人称NIO为Non-block IO。
它是基于I/O多路复用技术的非阻塞I/O,并不是异步的,NIO类库是JDK1.4中引入的,用来弥补原来的同步阻塞I/O的不足(BIO),有部分人称NIO为异步非阻塞IO,这里所说的”异步”是应用层的异步,系统内核的实现还是同步的。
JDK1.4阶段NIO的Selector底层基于select/poll模型实现,到了JDK1.5+ Selector的底层实现被优化,底层使用epoll替换了select/poll,但这只是NIO的性能优化,上层API并没有变化,也没有改变I/O模型,还是同步非阻塞I/O。
与BIO中Socket类和ServerSocket类相对应,NIO提供了SocketChannel和ServerSocketChannel两种不同的套接字通道实现,这两种新增的通道都支持阻塞和非阻塞两种模式。
阻塞模式使用非常简单,但是性能和可靠性不好,一般来说,低负载、低并发的应用程序可以选择阻塞I/O以降低程序复杂度,对于高负载、高并发的应用需要使用NIO的非阻塞模式。
NIO中客户端的连接操作和SocketChannel的读写操作都是异步的,如果没有可读写数据会直接返回,可以通过在Selector上注册OP_CONNECT等待后续结果,这样I/O通信线程就可以处理其他的链路,不需要像BIO那样被同步阻塞。
因为NIO中的Selector底层实现所使用的的I/O多路复用本质上都属于就是同步I/O,我们需要主动去轮询就绪Channel并根据其状态来进行对应的I/O操作。
点击查看NIO完整示例代码

NIO类库中多了很多新增的功能和概念,下面我们来介绍一下

Buffer — 缓冲区

Buffer是NIO中新增的一个对象,它包含一些要写入或者读出的数据,在NIO库中所有的数据都是用Buffer处理的,任何时候访问NIO中的数据都要通过Buffer进行操作,Buffer其本质是一个数组,但功能远比数组强大,它提供了对数据的结构化访问及维护读写位置等信息。
最常用的缓冲区是ByteBuffer,用于操作byte[],Java中除了Boolean类型,其他的基本类型都对应着一种缓冲区,如:ByteBuffer、CharBuffer、ShortBuffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer。

每个Buffer类都是Buffer接口的一个实例,除了ByteBuffer,其他类型的Buffer操作都一样,只是操作的数据类型不同而已,由于绝大多数标准I/O操作都使用ByteBuffer,所以ByteBuffer在具有和其他类型一样的操作之外,还提供了特有的操作,用来方便网络读写。

Channel — 通道

Channel是一个通道,网络数据通过Channel读取和写入,通道Channel与流Stream之间的区别是通道是双向的,流只能在一个方向移动,要么是输入InputStream要么是输出OutputStream,而通道既可以用来读数据也可以用来写数据,也可以同时进行。
Channel是全双工通道,更好的体现了底层操作系统的API,在Unix网络编程模型中,底层操作系统的通道都是全双工的,同时支持读写操作。

Selector — 多路复用器/选择器

Selector是Java NIO编程的基础,熟练的掌握Selector对于NIO编程至关重要,多路复用器提供选择已经就绪的任务的能力,简单来说就是Selector会不断轮训注册在其上的Channel,如果某个Channel上面发生读写事件,
这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续I/O操作。
一个Selector可以同时轮询多个Channel,由于JDK使用了epoll()代替传统的select实现,所以它并没有最大连接句柄的限制,这也就意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

AIO — 异步非阻塞I/O

NIO2.0引入的新的异步通道的感念,对应UNIX网络I/O模型中的异步IO模型,也就是AIO,它提供了异步文件通道和异步套接字通道的实现,CompletionHandler接口的实现类作为操作完成的回调。
它不需要像NIO编程那样创建一个独立的I/O线程处理读写事件,也不需要对注册的Channel进行轮询操作即可实现异步读写,从而大大简化了NIO的编程模型。
AIO的Socket操作都是由JDK底层线程池负责回调并驱动读写操作,所以使用AIO的异步非阻塞Channel进行编程比NIO变成更为简单。
点击查看AIO完整示例代码

Choice wechat
关注公众号,获取文章更新通知。
-------------本文结束感谢您的阅读-------------
坚持原创技术分享,您的支持将鼓励我继续创作!