第一章 Java I/O
1.1 I/O基础入门
Java1.4之前的版本,开发高性能I/O程序的时候,有问题:
- 没有数据缓冲区,I/O性能有问题
- 没有Channel概念,只有输入输出流
- 只有BIO,通常会导致通信线程被长时间阻塞
- 支持字符集有限,硬件移植性不好
1.1.1 Linux网络I/O模型
Linux的内核将所有的外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也有相应的描述符,称为socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。
Unix提供了五种I/O模型:
- 阻塞I/O模型:所有文件操作都是阻塞的,
- 非阻塞I/O模型:recvfrom时,如果缓冲区没有数据就返回一个EWOULDBLOCK错误,然后一直轮询检查
- I/O复用模型:Linux提供select/poll,进程通过将一个或多个fd传递给Select或poll,阻塞到Select上,这样就可以检测多个fd是否处于就绪状态。还提供了epoll,基于事件驱动代替select和poll的顺序扫描
- 信号驱动I/O模型:需要开启套接口信号驱动I/O功能,通过系统调用sigaction执行信号处理函数,接收到信号就回调然后recvfrom执行。
- 异步I/O:告知内核启动某个操作,内核完成操作后再通知。
1.1.2 I/O多路复用
可以同时处理多个客户端请求,把多个I/O阻塞复用到同一个Select的阻塞上,不需要新的进程或线程,系统开销小
select有缺陷,epoll改进:
-
一个进程打开的socketfd不受限制,仅受限于操作系统的最大文件句柄数
-
I/O效率不会随着FD的数目增加而线性下降
epoll只会对活跃的socket进行操作,因为epoll是根据每个fd的callback函数实现的,只有活跃的socket才会去主动调用这个函数。
-
使用mmap加速内核与用户空间的消息传递
-
epoll的api更加简单
1.2 Java的I/O演进
JDK1.4推出NIO,主要的类和接口:
- 缓冲区ByteBuffer
- 管道Pipe
- 进行I/O操作的Channel,包括ServerSocketChannel和SocketChannel
- 多种字符集的编码和解码能力
- 实现非阻塞I/O操作的多路复用器selector
- 基于流行的Perl实现的正则表达式类库
- 文件通道FileChannel
依旧有一些问题:
- 没有统一的文件属性
- api能力弱,目录的级联创建和递归遍历需要自己实现
- 底层存储系统的一些高级api无法使用
- 文件操作不支持异步读写
JDK1.7升级了NIO,
- 提供批量获取文件属性的API,api具有平台无关性
- 提供aio
- 完成通道功能
第二章 NIO入门
2.1 传统BIO编程
ServerSocket负责绑定IP地址,启动监听端口,Socket负责发起连接,连接成功后,双方通过输入和输出流进行同步阻塞式通信
BIO通信模型图
可以看出服务端线程个数和客户端并发访问数一比一关系
并发访问量增加会导致系统的性能急速下降
2.2 伪异步I/O编程
改进上述模型,通过一个线程池来处理多个客户端的请求接入,客户端个数可以远远大于线程池最大线程数
2.2.1 伪异步I/O模型图
采用线程池和任务队列可以实现
当接受到新的客户端连接时,将请求Socket封装成一个Task,调用线程池的execute方法执行,避免每个请求都创建一个新的线程
没有从根本解决问题:
当对Socket的输入流进行读取操作时,会一直阻塞。依赖于对方的处理速度,可靠性比较差
2.3 NIO编程
-
Non-Block I/O非阻塞式I/O
-
与ServerSocket和Socket对应,提供ServerSocketChannel和SocketChannel
2.3.1 NIO 类库
面向块的I/O,通过块处理数据
-
缓冲区 Buffer
- Buffer是一个对象,包含要写入或读取的数据。
- 实质是一个数组。通常是一个字节数组(ByteBuffer)
- 每一种Java基本类型(除了Boolean)都有对应的缓冲区
-
通道Channel
- 网络数据通过通道读取和写入,与流的不同之处在于Channel是双向的(读,写或同时),也就是全双工的,更好的映射了操作系统底层API
- 可分为两类:用于网络的SelectableChannel和用于文件的FileChannel
-
多路复用器Selector
- 具有选择已就绪任务的能力,也就是不断轮询注册在其上的Channel,如果某个Channel上面发生读或者写事件,这个Channel就处于就绪状态,就会被轮询出来然后通过SelectionKey获取就绪Channel的集合,进行I/O操作。
- 一个Selector可以同时轮询多个Channel,且使用epoll代替select实现,一个Selector就可以处理成千上万的客户端
2.3.2 NIO服务端序列图
服务端创建过程:
- 打开ServerSocketChannel,用于监听客户端连接
- 绑定监听端口,设置为非阻塞模式
- 创建Reactor线程,创建Selector并启动线程
- 将ServerSocketChannel注册到Selector上,监听ACCEPT事件
- Selector轮询准备就绪的Key
- 监听到有新的连接请求,处理新的接入请求,完成TCP三次握手,建立连接
- 设置客户端为非阻塞模式
- 将新的SocketChannel注册到Selector上
- 异步读取客户端请求消息到缓冲区
- 对ByteBuffer进行编解码,如果有半包消息指针reset,继续读取后续的报文,将解码成功的消息封装成Task,投递到业务线程池中,进行业务逻辑编排
- 将POJO对象encode成ByteBuffer,调用SocketChannel的异步write接口,将消息异步发送给客户端
2.3.3 服务端源码
- 在主线程中设定监听端口以及创建Server线程
- Server类中:
- 先构造方法,资源初始化,创建Selector和ServerSocketChannel,对Channel和TCP参数进行设置
- selector轮询,有处于就绪状态的Channel时注册到SelectionKey
- 处理客户端请求,根据SelectionKey判断类型,通过ServerSocketChannel的accept接受请求并创建SocketChannel
- 创建ByteBuffer读取客户端数据,调用SocketChannel的read方法读取码流,读取到Buffer以后进行解码。首先需要进行flip操作,然后再调用ByteBuffer的get操作将缓冲区可读的数据读出。
- 将消息回送给客户端:创建ByteBuffer,调用ByteBuffer的put方法放入ByteBuffer中,再进行flip操作,然后再通过SocketChannel的write方法写入Channel,传回给客户端。
2.3.4 NIO客户端序列图
步骤如图
2.3.5 客户端源码
- 主线程中创建ClientHandle线程
- ClientHandle:
- 构造函数初始化NIO的多路复用器和SocketChannel对象,将SocketChannel设置为异步非阻塞模式
- 发送连接请求,如果成功,将SocketChannel注册到Selector上,并注册SelectionKey为OP.READ,如果没有成功,注册Key为OP.CONNECT,如果服务端返回TCP syn-ack消息后,Selector就能轮询到这个SocketChannel处于就绪状态
- 轮询Selector,有就绪的Channel时,执行handleInput(key):
- 对SelectionKey判断,如果处于连接状态,调用SocketChannel的finishConnect方法,如果返回true就注册到Selector上,Key为OP.READ,失败则报出异常
- 对可读的SocketChannel进行读取,调用SocketChannel的read操作。完成后将stop置为true
- 对连接资源释放
优点
- 客户端发起的连接操作是异步的,可以通过在多路复用器注册OPCONNECT等待后续结果,不需要像之前的客户端那样被同步阻塞。
- SocketChannel的读写操作都是异步的,如果没有可读写的数据它不会同步等待直接返回,这样I0通信线程就可以处理其他的链路,不需要同步等待这个链路可用。
- 线程模型的优化:由于JDK的Selector在Linux等主流操作系统上通过epoll 实现,它没有连接句柄数的限制(只受限于操作系统的最大句柄数或者对单个进程的句柄限制),这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降。因此,它非常适合做高性能、高负载的网络服务器
2.4 AIO编程
异步非阻塞I/O,对应UNIX事件驱动I/O(AIO), 不需要通过Selector进行轮询操作就能够实现异步读写,实现比NIO简单
核心是异步通道(Asynchronous Channel),异步通道不会阻塞线程,I/O操作会立即返回,当当I/O操作完成时,系统会自动调用与之关联的CompletionHandler,或通过Future对象通知操作的完成。CompletionHandler是一个回调接口,用于处理异步操作的结果。当异步操作完成时,系统会自动调用相应的CompletionHandler方法。这个回调机制避免了线程在I/O操作上进行等待。
CompletionHandler的常用方法包括:
completed(V result, A attachment)
:异步操作成功完成时调用。failed(Throwable exc, A attachment)
:异步操作失败时调用。
除了CompletionHandler,Java AIO还支持使用Future对象来获取异步操作的结果。使用Future的方式类似于同步编程,但操作是异步进行的。
通过调用Future的get()
方法,线程可以阻塞直到操作完成,这种方式有时被用在需要混合同步和异步处理的场景中。
2.5 4种I/O对比
2.5.1 概念
-
异步非阻塞I/O
NIO不能称为异步非阻塞I/O,只能称为非阻塞I/O,是基于I/O复用技术的非阻塞I/O。
AIO是真正的异步I/O
-
多路复用器Selector
JavaNIO的实现关键是多路复用I0技术,多路复用的核心就是通过Selector来轮询注册在其上的Channel,当发现某个或者多个Channel处于就绪状态后,从阻塞状态返回就绪的Channel的选择键集合,进行I/0操作。
-
伪异步I/O
来源于实践,通过线程池做缓冲区
2.5.2 不同I/O模型对比
2.6 选择Netty
开发高质量的NIO程序很复杂,且调试和跟踪非常麻烦。
- NIO的类库和API繁杂,使用麻烦
- 需要具备其他的额外技能做铺垫,例如熟悉Java多线程编程。
- 可靠性能力补齐,工作量和难度都非常大。
- BUG
Netty优点:
- API使用简单,开发门槛低
- 功能强大,预置了多种编解码功能,支持多种主流协议
- 定制能力强,可以通过 ChannelHandler对通信框架进行灵活地扩展
- 性能高,通过与其他业界主流的NIO框架对比,Netty的综合性能最优
- 成熟、稳定,Netty修复了已经发现的所有JDK NIO BUG,业务开发人员不需要再为 NIO的BUG 而烦恼
- 社区活跃,版本迭代周期短
- 经历了大规模的商业应用考验,质量得到验证