JAVA IO

Linux网络I/O模型

       Linux的内核将所有外部设备都看作一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。
       根据UNIX网络编程对I/O模型的分类,UNIX提供了5种I/O模型:

  • 阻塞I/O模型:最常用的I/O模型,缺省条件下,所有文件操作都是阻塞的。以套接字接口为例:在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或者发生错误时才返回,在此期间都是被阻塞的。

    JAVA IO_第1张图片

  • 非阻塞I/O模型:recvfrom从应用层到内核的时候,如果该缓冲区没有数据,就直接返回一个EWOULDBLOCK错误,一般都对非阻塞I/O模型进行轮询检查这个状态,看内核是否有数据到来。

    JAVA IO_第2张图片

  • I/O复用模型:Linux提供select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,select/poll可以通过顺序扫描侦测多个fd是否处于就绪状态,不过支持的fd数量有限。Linux还提供了epoll,基于事件驱动方式代替顺序扫描,性能更高,当有fd就绪时,立即回调函数rollback

    JAVA IO_第3张图片

  • 信号驱动I/O模型:首先开启套接字信号驱动I/O功能,并通过系统调用sigaction执行信号处理函数(此系统调用立即返回,非阻塞)。当数据准备就绪时,为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。

    JAVA IO_第4张图片

  • 异步I/O模型:告知内核启动某个操作,并让内核在整个操作完成后(包括数据从内核复制到用户自己的缓冲区)进行通知。此模型与信号驱动模型的主要区别是:信号驱动I/O模型由内核通知我们何时开始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。

    JAVA IO_第5张图片

I/O多路复用技术

       I/O多路复用通过把多个I/O的阻塞复用到同一个select的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。优势是:系统开销小,无需创建和维护额外线程,降低了系统维护工作量,节省了系统资源。主要应用场景如下:

  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字;
  • 服务器需要同时处理多种网络协议的套接字;

       目前支持I/O多路复用的系统调用有select、pselect、poll、epoll,然而select有一些固有缺陷,为了克服select的缺点,epoll做了很多重大改进:

  • 支持一个进程打开的socket描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数);

传统的BIO

       网络编程的基本模型是Client/Server模型,也就是两个进程之间进行相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务器端监听地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字(Socket)进行通信。
       采用BIO通信模型的服务端,通常由一个独立的Acceptor线程负责监听客户端的连接,接收到客户端连接请求之后为每一个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流返回应答给客户端,线程销毁。此模型最大的问题就是缺乏弹性伸缩能力,当客户端并发访问量增加后,服务端的线程个数和客户端并发访问数呈1:1的正比关系,当线程数膨胀,系统性能将急剧下降。

伪异步I/O编程
       为解决同步阻塞I/O面临的一个I/O链路需要一个线程处理的问题,通过一个线程池来处理多个客户端的请求接入,形成客户端个数M : 线程池最大线程数N的比例关系。通过线程池可以灵活地调配线程资源,设置线程的最大值,防止由于海量并发接入导致的线程耗尽。

弊端分析
       当对Socket的输入流进行读取操作时,会一直阻塞直到发生下列三种事件,意味着当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞:

  • 有数据可读;
  • 可用数据已经读取完毕;
  • 发生空指针或者I/O异常;

       当写输出流时,将被阻塞直到所有要发送的字节全部写入或者发生异常。但当消息接收方处理缓慢时,其不能及时地从TCP缓冲区读取数据,这将导致发送发的TCP发送窗口不断减小,直到为0,虽然双方处于Keep-Alive状态,但发送方已经不能再向TCP缓冲区写入消息,这时若采用的是同步阻塞I/O,write操作将被无限期的阻塞,直到TCP的发送窗口大于0或者发生I/O异常。

NIO编程

1.缓冲区Buffer
       Buffer是一个对象,它包含一些要写入或者要读出的数据。实质是一个数组,通常为字节数组,并且提供了对数据的结构化访问以及维护读写位置等信息。

2.通道Channel
       通道与流不同之处在于通道是双向的,流只是在一个方向上移动,而通道可以用于读、写或者两者同时进行。

3.多路复用器Selector
       Selector会不断地轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,此Channel就处于就绪状态,然后通过SelectionKey获取就绪Channel集合,进行后续的I/O操作。JDK使用epoll()代替传统的select实现,所以它并没有最大连接句柄1024/2048的限制,这意味着只需要一个线程负责Selector的轮询,就可以接入成千上万的客户端。

4.NIO服务端序列图

  • 步骤一:打开ServerSocketChannel,监听客户端连接,它是所有客户端连接的父管道;
  • 步骤二:绑定监听端口,设置连接为非阻塞模式;
  • 步骤三:创建Reactor线程,创建多路复用器并启动线程;
  • 步骤四:将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听Accept事件;
  • 步骤五:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key;
  • 步骤六:多路复用器监听到新的客户端接入请求,处理新的接入请求,完成TCP三次握手,建立物理链路;
  • 步骤七:设置客户端链路模式为非阻塞;
  • 步骤八:将新接入的客户端连接注册到Rector线程的多路复用器上,监听读操作,读取客户端发送的网络消息;
  • 步骤九:异步读取客户端请求消息到缓冲区;
  • 步骤十:对ByteBuffer进行编解码,如果有半包消息,指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排;
  • 步骤十一:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端。

注意:如果发送区TCP缓冲区满,会导致写半包,此时需要注册监听写操作位,循环写,知道整包消息写入TCP缓冲区。

5.NIO客户端序列图

  • 步骤一:打开SocketChannel,绑定客户端本地地址(默认会随机分配一个可用的本地地址);
  • 步骤二:设置SocketChannel为非阻塞模式,同时设置客户端连接的TCP参数;
  • 步骤三:异步连接客户端;
  • 步骤四:判断是否连接成功,若成功,则直接注册读状态位到多路复用器中,若未连接成功(异步连接),说明客户端已经发送了sync包,但服务端还未返回ack包,物理链路还未建立,则注册连接状态到多路复用器中,监听服务端的TCP ACK应答;
  • 步骤五:创建Reactor线程,创建多路复用器并启动线程;
  • 步骤六:多路复用器在线程run方法的无限循环体内轮询准备就绪的Key;
  • 步骤七:接受connect事件进行处理;
  • 步骤八:判断连接结果,若连接成功,注册读事件到多路复用器;
  • 步骤九:异步读客户端请求消息到缓冲区;
  • 步骤十:将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送到客户端。

AIO编程

       NIO 2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。提供以下两种方式获取操作结果:

  • 通过java.util.concurrent.Future类来表示异步操作的结果;
  • 在执行异步操作的时候传入一个java.nio.channels

       NIO 2.0的异步套接字通道是真正的异步非阻塞I/O,对应于UNIX网络编程的事件驱动I/O(AIO)。不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写,从而简化了NIO的编程模型。

       TCP以流的方式进行数据传输,上层的应用程序为了对消息进行区分,往往采用如下4种方式:

  • 消息长度固定,累计读取到长度总和为定长LEN的报文后,就认为读取到了一个完整的消息;将计数器置位,重新开始读取下一个数据包;
  • 将回车换行符作为消息结束符;
  • 将特殊的分隔符作为消息的结束标志,如回车换行符;
  • 通过在消息头中定义长度字段来标识消息的总长度;

你可能感兴趣的:(JAVA IO)