背景介绍在互联网的时代下绝大部分数据都是通过网络来进行获取的。在服务端的架构中绝大部分数据也是通过网络来进行交互的。而且作为服务端的开发工程师来说都会进行一系列服务设计、开发以及能力开放而服务能力开放也是需要通过网络来完成的因此对网络编程以及网络IO模型都不会太陌生。由于有很多优秀的框架比如Netty、HSF、Dubbo、Thrift等已经把底层网络IO给封装了通过提供的API能力或者配置就能完成想要的服务能力开发因此大部分工程师对网络IO模型的底层不够了解。本文系统的讲解了Linux内核的IO模型、Java网络IO模型以及两者之间的关系什么是IO我们都知道在Linux的世界一切皆文件。而文件就是一串二进制流不管Socket、FIFO、管道还是终端对我们来说一切都是流。在信息的交换过程中我们都是对这些流进行数据收发操作简称为I/O操作。往流中读取数据系统调用Read写入数据系统调用Write。通常用户进程的一个完整的IO分为两个阶段磁盘IO网络IO操作系统和驱动程序运行在内核空间应用程序运行在用户空间两者不能使用指针传递数据因为Linux使用的虚拟内存机制必须通过系统调用请求内核来完成IO动作。IO有内存IO、网络IO和磁盘IO三种通常我们说的IO指的是后两者为什么需要IO模型如果使用同步的方式来通信的话所有的操作都在一个线程内顺序执行完成这么做缺点是很明显的因为同步的通信操作会阻塞同一个线程的其他任何操作只有这个操作完成了之后后续的操作才可以完成所以出现了同步阻塞多线程每个Socket都创建一个线程对应但是系统内线程数量是有限制的同时线程切换很浪费时间适合Socket少的情况。因该需要出现IO模型。Linux的IO模型在描述Linux IO模型之前我们先来了解一下Linux系统数据读取的过程以用户请求index.html文件为例子说明基本概念用户空间和内核空间操作系统的核心是内核独立于普通的应用程序可以访问受保护的内存空间也有访问底层硬件设备的所有权限。为了保证内核的安全用户进程不能直接操作内核操作系统将虚拟空间划分为两部分一部分为内核空间一部分为用户空间。进程切换为了控制进程的执行内核必须有能力挂起正在CPU上运行的进程并恢复以前挂起的某个进程的执行。这种行为被称为进程切换。因此可以说任何进程都是在操作系统内核的支持下运行的是与内核紧密相关的。进程的阻塞正在执行的进程由于期待的某些事件未发生如请求系统资源失败、等待某种操作的完成、新数据尚未到达或无新工作做等则由系统自动执行阻塞原语(Block)使自己由运行状态变为阻塞状态。可见进程的阻塞是进程自身的一种主动行为也因此只有处于运行态的进程获得CPU才可能将其转为阻塞状态。当进程进入阻塞状态是不占用CPU资源的。文件描述符文件描述符File Descriptor是计算机科学中的一个术语是一个用于表述指向文件的引用的抽象化概念。文件描述符在形式上是一个非负整数实际上它是一个索引值指向内核为每一个进程所维护的该进程打开文件的记录表。当程序打开一个现有文件或者创建一个新文件时内核向进程返回一个文件描述符。缓存IO大多数文件系统的默认 IO 操作都是缓存 IO。其读写过程如下读操作操作系统检查内核的缓冲区有没有需要的数据如果已经缓存了那么就直接从缓存中返回否则从磁盘、网卡等中读取然后缓存在操作系统的缓存中写操作将数据从用户空间复制到内核空间的缓存中。这时对用户程序来说写操作就已经完成至于什么时候再写到磁盘、网卡等中由操作系统决定除非显示地调用了 sync 同步命令。假设内核空间缓存无需要的数据用户进程从磁盘或网络读数据分两个阶段阶段一内核程序从磁盘、网卡等读取数据到内核空间缓存区阶段二用户程序从内核空间缓存拷贝数据到用户空间。缓存 IO 的缺点数据在传输过程中需要在应用程序地址空间和内核空间进行多次数据拷贝操作这些数据拷贝操作所带来的CPU以及内存开销非常大。同步阻塞用户空间的应用程序执行一个系统调用这会导致应用程序阻塞什么也不干直到数据准备好并且将数据从内核复制到用户进程最后进程再处理数据在等待数据到处理数据的两个阶段整个进程都被阻塞不能处理别的网络IO。调用应用程序处于一种不再消费 CPU 而只是简单等待响应的状态因此从处理的角度来看这是非常有效的。这也是最简单的IO模型在通常FD较少、就绪很快的情况下使用是没有问题的。同步非阻塞非阻塞的系统调用调用之后进程并没有被阻塞内核马上返回给进程如果数据还没准备好此时会返回一个error。进程在返回之后可以干点别的事情然后再发起系统调用。重复上面的过程循环往复的进行系统调用。这个过程通常被称之为轮询。轮询检查内核数据直到数据准备好再拷贝数据到进程进行数据处理。需要注意拷贝数据整个过程进程仍然是属于阻塞的状态。这种方式在编程中对Socket设置O_NONBLOCK即可。IO多路复用IO多路复用这是一种进程预先告知内核的能力让内核发现进程指定的一个或多个IO条件就绪了就通知进程。使得一个进程能在一连串的事件上等待。IO复用的实现方式目前主要有Select、Poll和Epoll。伪代码描述IO多路复用while(status OK) { // 不断轮询 ready_fd_list io_wait(fd_list); //内核缓冲区是否有准备好的数据 for(fd in ready_fd_list) { data read(fd) // 有准备好的数据读取到用户缓冲区 process(data) } }信号驱动首先我们允许Socket进行信号驱动IO并安装一个信号处理函数进程继续运行并不阻塞。当数据准备好时进程会收到一个SIGIO信号可以在信号处理函数中调用I/O操作函数处理数据。流程如下开启套接字信号驱动IO功能系统调用Sigaction执行信号处理函数非阻塞立刻返回数据就绪生成Sigio信号通过信号回调通知应用来读取数据此种IO方式存在的一个很大的问题Linux中信号队列是有限制的如果超过这个数字问题就无法读取数据异步非阻塞异步IO流程如下所示当用户线程调用了aio_read系统调用立刻就可以开始去做其它的事用户线程不阻塞内核就开始了IO的第一个阶段准备数据。当内核一直等到数据准备好了它就会将数据从内核内核缓冲区拷贝到用户缓冲区内核会给用户线程发送一个信号或者回调用户线程注册的回调接口告诉用户线程Read操作完成了用户线程读取用户缓冲区的数据完成后续的业务操作相对于同步IO异步IO不是顺序执行。用户进程进行aio_read系统调用之后无论内核数据是否准备好都会直接返回给用户进程然后用户态进程可以去做别的事情。等到数据准备好了内核直接复制数据给进程然后从内核向进程发送通知。对比信号驱动IO异步IO的主要区别在于信号驱动由内核告诉我们何时可以开始一个IO操作(数据在内核缓冲区中)而异步IO则由内核通知IO操作何时已经完成(数据已经在用户空间中)。异步IO又叫做事件驱动IO在Unix中为异步方式访问文件定义了一套库函数定义了AIO的一系列接口。使用aio_read或者aio_write发起异步IO操作使用aio_error检查正在运行的IO操作的状态。目前Linux中AIO的内核实现只对文件IO有效如果要实现真正的AIO需要用户自己来实现。目前有很多开源的异步IO库例如libevent、libev、libuv。Java网络IO模型BIOBIO是一个典型的网络编程模型是通常我们实现一个服务端程序的方法对应Linux内核的同步阻塞IO模型发送数据和接收数据的过程如下所示步骤如下主线程accept请求请求到达创建新的线程来处理这个套接字完成对客户端的响应主线程继续accept下一个请求服务端处理伪代码如下所示这是经典的一个连接对应一个线程的模型之所以使用多线程主要原因在于socket.accept()、socket.read()、socket.write()三个主要函数都是同步阻塞的。当一个连接在处理I/O的时候系统是阻塞的如果是单线程的话必然就阻塞但CPU是被释放出来的开启多线程就可以让CPU去处理更多的事情。其实这也是所有使用多线程的本质利用多核当I/O阻塞时但CPU空闲的时候可以利用多线程使用CPU资源。当面对十万甚至百万级连接的时候传统的BIO模型是无能为力的。随着移动端应用的兴起和各种网络游戏的盛行百万级长连接日趋普遍此时必然需要一种更高效的I/O处理模型。NIOJDK1.4开始引入了NIO类库主要是使用Selector多路复用器来实现。Selector在Linux等主流操作系统上是通过IO复用Epoll实现的。NIO的实现流程类似于Select创建ServerSocketChannel监听客户端连接并绑定监听端口设置为非阻塞模式创建Reactor线程创建多路复用器(Selector)并启动线程将ServerSocketChannel注册到Reactor线程的Selector上监听Accept事件Selector在线程run方法中无线循环轮询准备就绪的KeySelector监听到新的客户端接入处理新的请求完成TCP三次握手建立物理连接将新的客户端连接注册到Selector上监听读操作读取客户端发送的网络消息客户端发送的数据就绪则读取客户端请求进行处理简单处理模型是用一个单线程死循环选择就绪的事件会执行系统调用Linux 2.6之前是Select、Poll2.6之后是EpollWindows是IOCP还会阻塞的等待新事件的到来。新事件到来的时候会在Selector上注册标记位标示可读、可写或者有连接到来简单处理模型的伪代码如下所示NIO由原来的阻塞读写占用线程变成了单线程轮询事件找到可以进行读写的网络描述符进行读写。除了事件的轮询是阻塞的没有可干的事情必须要阻塞剩余的I/O操作都是纯CPU操作没有必要开启多线程。并且由于线程的节约连接数大的时候因为线程切换带来的问题也随之解决进而为处理海量连接提供了可能。AIOJDK1.7引入NIO2.0提供了异步文件通道和异步套接字通道的实现。其底层在Windows上是通过IOCP实现在Linux上是通过IO复用Epoll来模拟实现的。在JAVA NIO框架中Selector它负责代替应用查询中所有已注册的通道到操作系统中进行IO事件轮询、管理当前注册的通道集合定位发生事件的通道等操作。但是在JAVA AIO框架中由于应用程序不是轮询方式而是订阅-通知方式所以不再需要Selector选择器了改由Channel通道直接到操作系统注册监听 。JAVA AIO框架中只实现了两种网络IO通道AsynchronousServerSocketChannel服务器监听通道AsynchronousSocketChannelSocket套接字通道。具体过程如下所示创建AsynchronousServerSocketChannel绑定监听端口调用AsynchronousServerSocketChannel的accpet方法传入自己实现的CompletionHandler包括上一步都是非阻塞的连接传入回调CompletionHandler的completed方法在里面调用AsynchronousSocketChannel的read方法传入负责处理数据的CompletionHandler数据就绪触发负责处理数据的CompletionHandler的completed方法继续做下一步处理即可写入操作类似也需要传入CompletionHandler最后觉得有收获可以关注点赞转发下哈谢谢谢谢