第1章 Java I/O 的演进之路
I/O多路复用技术: 当需要同时处理多个客户端接入请求时, 可以利用多线程或者I/O多路复用技术进行处理.
I/O 多路复用技术通过把多个I/O的阻塞复用到同一个select的阻塞上, 从而使得系统在单线程的情况下可以同时处理多个服务端请求. 减少系统开销, 系统不需要创建新的额外进程或者线程.
应用场景
- 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
- 服务器需要同时处理多种网络协议的套接字
epoll
克服了select
的缺点
- 支持一个进程打开的socket描述符 不受限制(仅首先于操作系统最大文件句柄数)
- I/O效率不会随着FD数目的增加而线性下降
- 使用mmap加速内核与用户空间的消息传递
- epoll的API更加简单
Java.NIO包
- 异步I/O操作的缓冲区ByteBuffer等
- 异步I/O操作的管道Pipe
- 进行各种I/O操作的Channel, 包括ServerSocketChannel和SocketChannel
- …
Java.NIO包的主要问题
- 没有统一的文件属性(例如读写权限)
- API能力比较弱, 例如目录的级联创建和递归遍历, 往往需要自己实现
- 底层存储系统的一些高级API无法使用
- 所有的文件操作都是同步阻塞调用, 不支持异步文件读写操作
第2章 NIO入门
BIO编程
- Client/Server
- 三次握手, socket通信
- 通信模型: 通常由一个独立的Acceptor线程负责监听客户端的连接, 接受请求之后为每个客户端创建一个新的线程进行链路处理, 处理完后通过输出流返回应答给客户端
弊端
- 缺乏弹性伸缩能力
- 访问量增加后, 服务端的线程个数和客户端并发访问数呈1:1的关系, 线程是java虚拟机宝贵的系统资源, 程数膨胀会让系统的性能急剧下降
在高性能服务器应用, 需要面向成千上万个客户端的并发连接, 这种模型显然无法满足高性能, 高并发接入.
伪异步I/O编程
改进线程链接1:1模型, 演进出了一种通过线程池或者消息队列实现 1个或者多个线程处理N个客户端的模型, 由于它的底层通信机制依然使用同步阻塞I/O, 所以被称为“伪同步”.
- 连接线程比 M:N, M大于N
- 当有新的客户端接入时, 将客户端的Socket封装成一个Task (该任务实现Java.lang.runnable接口)投递到后端的线程池中进行处理, JDK的线程池维护一个消息队列和N个活跃线程, 对消息队列中的人物进行处理. 由于线程池可以设置消息队列的大小和最大线程数, 因此它的资源占用是可控的, 无论多少个客户端并发访问,都不会导致资源耗尽和宕机
- 采用了线程池实现, 避免了为每个请求都创建一个独立线程造成的线程资源耗尽问题. 但是由于底层仍然是同步阻塞模型, 因此无法从根本上解决问题.
弊端
当对Socket的输入流进行读取操作的时候, 它会一直阻塞下去, 知道发生以下三种事件:
- 有数据可读
- 可用数据已经读取完毕
- 发生空指针或者I/O异常
这意味着当对方操作比较缓慢或者网络传输缓慢时, 读取输入流的一方的通信线程将被长时间阻塞, 如果对方要60s才能够将数据发送完成, 读取一方的I/O线程也将会被同步阻塞60s, 其他只能排队.资源利用率低下.
NIO编程
缓冲区(Buffer) 是与原I/O的一个重要区别, 在面向流I/O中, 可以将数据直接写入或则将数据直接读到Stream对象中. NIO库中 都是用缓冲区来操作. 实际上, 缓冲区市个字节数组 bytebuffer
通道(Channel) 像是自来水管, 网络数据通过Channel读取和写入. 通道与流不同之处在于通道是双向的, 流是单向的, 通道可以读写同时 Channel是全双工, 比流更好映射底层操作系统的API.
多路复用Selector
Selector会不断地轮询(poll) 注册在其上的Channel, 如果某个Channel上面发生读或者些事件, 这个channel就处于就绪状态, 会被Selector轮询出来, 然后通过selectionKey可以获得就绪Channel的集合,进行后续的I/O操作
一个多路复用Selector可以同时轮训多个channel, JDK采用了epoll() 代替传统的select实现, 所以它并没有最大连接句柄1024/2048 限制, 这也就意味着只需要一个线程负责Selector的轮询, 就可以接入成千上万的客户端.
NIO服务端通信序列图
NIO客户端创建序列图
NIO编程的难度确实比同步阻塞BIO大很多. 尽管难度增加, 但是不影响程序员对其使用, 因为以下优点:
优点
- 客户端发起的连接操作是异步的, 可以通过在多路复用器注册OP_CONNECT等后续结果, 不需要像之前的客户端那样被同步阻塞.
- SockChannel的读写操作都是异步的, 如果没有可读写的数据它不会同步等待, 直接返回, 这样I/O通信线程就可以处理别的链路, 不需要同步等待这个链路可用.
- 线程模型的优化 由于JDK的Selector在Linux等主流操作系统上通过epoll实现, 它没有连接句柄数的限制, 意味着一个selector线程可以同时处理成千上万客户端连接, 而且性能不会有所折损
AIO编程
引入了异步通道的概念, 并提供异步文件通道和异步套接字通道的实现. 异步通道提供两种方式获取获取操作结果
- 通过java.util.concurrent.Future类
- 在执行异步操作的时候传入一个java.nio.channels
CompletionHandler 接口
的实现类作为操作完成的回调. 异步Socket Channel
是被动执行对象, 我们不需要像NIO编程那样创建一个独立的I/O线程来处理读写操作. 对于AsynchronousServerSocketChannel
和AsynchronousSocketChannel
, 它们都由JDK底层的线程池负责回调并驱动读写操作. 正因为如此, 基于NIO2.0新的异步非阻塞Channel会比NIO简单.
模型对比
为何选择Netty
不选择原生IO
- NIO的类库 API繁杂 需要熟练掌握Selector, ServerSocketChannel, SocketChannel, ByteBuffer
- 需要其他前置的技能, java多线程.
- 可靠性能力补齐, 工作量和难度都非常大
- JDK NIO的bug, 比如 epoll bug 会导致Selector空轮询
选择NIO
业界中最流行NIO
- API简单
- 功能强大
- 定制能力强
- 性能高
- …