同步阻塞 I/O(BIO)
同步阻塞 I/O,服务器实现模式为一个连接一个线程,即客户端有连接请求时服务器就需要启动一个线程进行处理,如果这个连接不做任何事情会造成不必要的线程开销,可以通过线程池机制来改善。
BIO 方式适用于连接数目比较小且固定的架构,这种方式对服务端资源要求比较高,并发局限于应用中,在 jdk1.4 以前是唯一的 io 选择,但程序直观简单易理解。
BIO 图解:
伪异步模型 IO
也被成为 M:N 客户服务模型。即通过线程池模型的形式用 M 个线程来服务 N 个客户端的连接;其中 M 的大小可以根据服务器的配置来设置最大值,而可服务客户端个数 N 则可以远远的大于 M。这样来提高服务器的服务效率,提高线程利用率。同 BIO 模型类似,只不过,Acceptor 接受客户端请求后不再独立启动线程来处理,而是将客户请求交给线程池来处理,从而减少线程的创建数量,提高线程利用率,增加服务器的处理能力。
伪异步 IO 图解:
同步非阻塞 I/O(NIO)
同步非阻塞 I/O,服务器实现模式为一个请求一个线程,即客户端发送的连接请求都会注册到多路复用器上,多路复用器轮询到连接有 IO 请求时才启动一个线程进行处理。NIO 方式适用于连接数目多且连接比较短(轻操作)的架构,比如聊天服务器,并发局限于应用中,编程比较复杂,jdk1.4 开始支持。
I/O 多路复用模型
I/O 多路复用:I/O 就是指的我们网络 I/O,多路指多个 TCP 连接(或多个 Channel),复用指复用一个或少量线程。串起来理解就是很多个网络 I/O 复用一个或少量的线程来处理这些连接。
多路复用的优势并不是单个连接处理的更快,而是在于能处理更多的连接。
目前的常用的 IO 复用模型有三种:select、poll、epoll。
I/O 多路复用就是通过一种机制,一个进程可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但 select、poll、epoll 本质上都是同步 I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步 I/O 则无需自己负责进行读写,异步 I/O 的实现会负责把数据从内核拷贝到用户空间。
jdk1.4 是使用的 select/poll 模型,jdk1.5 以后把 select/poll 改为了 epoll 模型。
select 模型
各个客户端连接的文件描述符(fd)也就是套接字,都被放到了一个集合中,调用 select 函数之后会一直监视这些文件描述符中有哪些可读,如果有可读的描述符那么我们的工作进程就去读取资源。
我们在 select 函数中告诉内核需要监听的不同状态的文件描述符以及能接受的超时时间,函数会返回所有状态下就绪的描述符的个数,并且可以通过遍历 fdset,来找到就绪的文件描述符。
存在的问题:
- 每次调用 select,都需要把待监控的 fd 集合从用户态拷贝到内核态,当 fd 很大时,开销很大。
- 每次调用 select,都需要轮询一遍所有的 fd,查看就绪状态。这个开销在 fd 很多时也很大。
- select 支持的最大文件描述符数量有限,默认是 1024。
poll 模型
相对于 select,poll 已不存在最大文件描述符限制。
epoll 模型
epoll 在 Linux2.6 内核正式提出,是基于事件驱动的 I/O 方式,相对于 select 来说,epoll 没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的 copy 只需一次(可以理解为一块公共内存,该内存既不属于用户态也不属于内核态)。
select/poll 的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统 CPU 利用率。原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核 IO 事件异步唤醒而加入 Ready 队列的描述符集合就行了。
epoll 的好处:
- 避免内存级拷贝;
- 事件驱动(不是轮询);
但是也并不是所有情况下 epoll 都比 select/poll 好,比如在如下场景:
在大多数客户端都很活跃的情况下,系统会把所有的回调函数都唤醒,所以会导致负载较高。既然要处理这么多的连接,那倒不如 select/poll 遍历简单有效。
select & poll & epoll 比较
NIO 的 3 个核心概念
缓冲区 Buffer:Buffer 是一个对象。它包含一些要写入或者读出的数据。在面向流的 I/O 中,可以将数据写入或者将数据直接读到 Stream 对象中。
在 NIO 中,所有的数据都是用缓冲区处理。IO 是面向流的,NIO 是面向缓冲区的。
最常用的缓冲区是 ByteBuffer,一个 ByteBuffer 提供了一组功能用于操作 byte 数组。除了 ByteBuffer,还有其他的一些缓冲区,事实上,每一种 Java 基本类型(除了 Boolean)都对应一种缓冲区,具体如下:
- ByteBuffer:字节缓冲区;
- CharBuffer:字符缓冲区;
- ShortBuffer:短整型缓冲区;
- IntBuffer:整型缓冲区;
- LongBuffer:长整型缓冲区;
- FloatBuffer:浮点型缓冲区;
- DoubleBuffer:双精度浮点型缓冲区;
通道 Channel:Channel 是一个通道,可以通过它读取和写入数据,他就像自来水管一样,网络数据通过 Channel 读取和写入。
通道和流不同之处在于通道是双向的,流只是在一个方向移动,而且通道可以用于读,写或者同时用于读写。
因为 Channel 是全双工的,所以它比流更好地映射底层操作系统的 API,特别是在 UNIX 网络编程中,底层操作系统的通道都是全双工的,同时支持读和写。
Channel 有四种实现:
- FileChannel:是从文件中读取数据;
- DatagramChannel:从 UDP 网络中读取或者写入数据;
- SocketChannel:从 TCP 网络中读取或者写入数据;
- ServerSocketChannel:允许你监听来自 TCP 的连接,就像服务器一样,每一个连接都会有一个 SocketChannel 产生;
多路复用器 Selector:Selector 选择器可以监听多个 Channel 通道感兴趣的事情,read、write、accept(服务端接收)、connect,实现一个线程管理多个 Channel,节省线程切换上下文的资源消耗。Selector 只能管理非阻塞的通道,FileChannel 是阻塞的,无法管理。
关键对象:
Selector:选择器对象,通道注册、通道监听对象和 Selector 相关。
SelectorKey:通道监听关键字,通过它来监听通道状态。
监听注册在 Selector:
1socketChannel.register(selector, SelectionKey.OP_READ);
监听的事件:
- OP_ACCEPT:接收就绪,serviceSocketChannel 使用的;
- OP_READ:读取就绪,socketChannel 使用;
- OP_WRITE:写入就绪,socketChannel 使用;
- OP_CONNECT:连接就绪,socketChannel 使用;
NIO 的应用和框架:
Java NIO 成功的应用在了各种分布式、即时通信和中间件 Java 系统中,充分的证明了基于 NIO 构建的通信基础,是一种高效,且扩展性很强的通信架构。例如:Dubbo(服务框架),就默认使用 Netty 作为基础通信组件,用于实现各进程节点之间的内部通信。Jetty、Mina、Netty、Dubbo、ZooKeeper 等都是基于 NIO 方式实现。Mina 出身于开源界的大牛 Apache 组织 Netty 出身于商业开源大亨 Jboss Dubbo 阿里分布式服务框架。
异步非阻塞 I/O(AIO)
服务器实现模式为一个有效请求一个线程,客户端的 I/O 请求都是由 OS 先完成了再通知服务器应用去启动线程进行处理。AIO 方式适用于连接数目多且连接比较长(重操作)的架构,比如相册服务器,充分调用 OS 参与并发操作,编程比较复杂。
AIO 又称为 NIO2,在 JDK7 才开始支持。
作者:ens
链接:https://juejin.im/post/5e894a32e51d4546f87858c1