BIO
通信模型
一请求一应答
一客户端一线程
通常有一个Acceptor 线程负责监听客户端的连接,它接收到客户端连接请求之后为每个客户端创建一个新的线程进行链路处理,处理完成之后,通过输出流应答给客户端,线程销毁。
缺点
- 并发访问量增大后,线程数膨胀,系统性能急剧下降
伪异步I/O
对同步阻塞I/O进行优化,后端通过一个线程池来处理多个客户端的请求接入。
优点:
+ 通过线程池灵活的调配线程资源,设置线程的最大值,防止由于海量并发接入导致线程耗尽。
通信模型
当新的客户端接入时,将客户端的Socket封装成一个Task(实现Runnable接口)放到后端的线程池进行处理,JDK线程池维护了一个消息队列和一些活跃线程,对消息队列中的任务进行处理。
弊端分析
结合代码进行分析:
InputStream 源码
/**
* Reads the next byte of data from the input stream. The value byte is
* returned as an <code>int</code> in the range <code>0</code> to
* <code>255</code>. If no byte is available because the end of the stream
* has been reached, the value <code>-1</code> is returned. This method
* blocks until input data is available, the end of the stream is detected,
* or an exception is thrown.
*
* <p> A subclass must provide an implementation of this method.
*
* @return the next byte of data, or <code>-1</code> if the end of the
* stream is reached.
* @exception IOException if an I/O error occurs.
*/
public abstract int read() throws IOException;
注意第5,6行,当对Socket的输入流进行读操作的时候,它会一直阻塞下去,直到发生以下三种事件:
+ 有数据可读
+ 可用数据已经读取完毕
+ 发生空指针或者 I/O 异常
当对方发送请求或者应答消息比较缓慢,或者网络传输较慢时,读取输入流一方的通信线程将被长时间阻塞。
读写操作都是同步阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。
伪异步I/O只是对之前BIO的一个简单优化,无法从根本上解决同步I/O导致的通信线程阻塞问题。
NIO 编程
NIO : Non-block I/O 非阻塞I/O
缓冲区 Buffer
Buffer是一个对象,它包含一些要写入或要读出的数据。在读取数据时,数据直接读到缓冲区中;在写数据时,写入缓冲区中。
缓冲区实质上是一个数组。通常是一个字节数组(ByteBuffer)。它不仅仅是个数组,还提供了对数据的结构化访问以及维护读写位置等信息。
通道 Channel
通道与流的不同之处在于通道是双向的,流只是一个方向上移动,通道可用于读、写或二者同时进行。
实际上Channel可以分为两大类:
+ 用于网络读写的SelectableChannel
+ 用于文件操作的FileChannel
常涉及到的ServerSocketChannel和SocketChannel都是SelectableChannel的子类。
多路复用器 Selector
Selector会不断轮询注册在其上的Channel,如果某个Channel上面发生读写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪Channel的集合,进行后续的I/O操作。
由于JDK使用了epoll()代替了传统的select实现,所以它并没有最大连接句柄的限制。这就意味着只需一个线程负责Slector的轮询,就可以接入成千上万的客户端。
NIO 服务端说明
服务端通信序列图:
服务端创建步骤详解:
- 打开ServerSocketChannel ,用于监听客户端的连接,它是所有客户端的父管道
- 绑定监听端口,设置连接为非阻塞模式
- 创建Reactor线程,创建多路复用器并启动线程
- 将ServerSocketChannel注册到Reactor线程的多路复用器Selector上,监听Accept事件
- Selector在线程run方法的无限循环体内轮询准备就绪的Key
- Selector监听到有新的客户端接入,处理新的请求,完成TCP三次握手,建立物理链路
- 设置客户端链路为非阻塞模式
- 将新接入的客户端连接注册到Reactor线程的Selector上,监听读操作,读取客户端发送的网络消息
- 异步读取客户端请求到缓冲区
- 对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,放到线程池中
- 将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端
NIO 客户端说明
客户端创建序列图:
客户端创建步骤:
- 打开SocketChannel, 绑定客户端本地地址
- 设置SocketChannel 为非阻塞模式,同时设置连接的的TCP参数
- 异步连接服务器
- 判断是否连接成功,如果连接成功,则直接注册读状态到Selector。
- 向Reactor线程的Selector注册OP_CONNECT状态位,监听服务端的TCP ACK 应答
- 创建Reactor线程,创建Selector并启动线程
- Selector在线程run方法的无限循环体内轮询准备就绪的Key
- 接收connect事件进行处理
- 判断连接结果,如果连接成功,注册读事件到Selector
- 注册读事件到Selector
- 异步读客户端请求消息到缓冲区
- 对ByteBuffer进行编解码
- 将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送到客户端。
NIO 优点总结
- 客户端发起的连接操作是异步的
- SocketChannel的读写操作是异步的
- 线程模型的优化,JDK的Selector在Linux等主流操作系统上通过epoll实现,没有连接句柄数的限制
AIO 编程
NIO 2.0 引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现。
异步通道提供了以下两种方式来获取操作结果:
- 通过 java.util.concurrent.Future 类来表示异步操作的结果
- 在执行异步操作的时候传入一个 java.nio.channels
CompletionHandler 接口的实现类作为操作完成的回调。
NIO 2.0 的异步套接字通道是真正的异步非阻塞I/O,对应于UNIX网络编程中的事件驱动I/O (AIO)。它不需要通过Selector 对注册的通道进行轮询操作即可实现异步读取。
四种I/O的对比
. | 同步阻塞I/O(BIO) | 伪异步I/O | 非阻塞I/O(NIO) | 异步I/O(AIO) |
---|---|---|---|---|
客户端个数 | 1:1 | M:N | M:1(1个I/O线程处理多个客户端连接) | M:0(不需要额外的I/O线程,被动回调) |
I/O类型(阻塞) | 阻塞I/O | 阻塞I/O | 非阻塞I/O | 非阻塞I/O |
I/O类型(同步) | 同步I/O | 同步I/O | 同步I/O | 异步I/O |
API使用难度 | 简单 | 简单 | 非常复杂 | 复杂 |
调试难度 | 简单 | 简单 | 复杂 | 复杂 |
可靠性 | 非常差 | 差 | 高 | 高 |
吞吐量 | 低 | 中 | 高 | 高 |