Java语言IO演化流程
早期的java语言对IO的支持并不完善,在java1.4之后才提供了NIO类库,并且在1.7之后提供了NIO2.0类库,对NIO类库的功能做了一些扩展
1.4版本以前:
- 没有数据缓冲区的概念
- 没有channel的概念,只有输入输出流
- 采用的是同步阻塞IO(BIO),会导致通信线程被长时间阻塞
- 支持的字符集有限,扩展性低
1.4版本~1.7版本
- 新增java.nio包
- 提供了ButeBuffer、Channel、Selector以及多种字符集编解码的能力
1.7版本以后
- nio升级为nio2.0版本
- 新增批量获取文件属性api
- 提供aio(异步IO功能),支持基于文件以及网络套接字的异步IO操作
-
Linux网络IO模型简介
linux内核将所有外部设备都当成一个文件来操作,对文件的读写操作会调用内核提供的系统命令,然后返回一个file descriptor(fd 文件描述符)。同理,对一个socket的读写,也会返回一个socketfd(socket描述符)。描述符本质上来说是一个数字,指向内核中的一个结构体(文件路径、数据区等一些属性)
UNIX网络编程IO模型分类: 阻塞I/O模型
在进程空间中调用recvfrom,其系统调用直到数据包到达且被复制到应用进程的缓冲区中或发生错误时才返回,在此期间会一直等待,进程在recvfrom开始到返回的整段时间都是阻塞的,因此被称为阻塞I/O
- 非阻塞I/O模型
recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,会返回一个EWOULDBLOCK错误,一般都对
非阻塞I/O模型进行轮询检查这个状态
- 多路复用I/O模型
linux提供select、poll、epoll系统。
进程将一个或多个fd交给select、poll系统调用,阻塞在select操作上,这样select、poll系统通过顺序扫描来获取就绪的fd,但是select、poll系统支持的fd数量有限,这使得它的使用收到了一定的制约
epoll系统是通过事件驱动的方式代替顺序扫描,当有fd就绪时,立即回调函数rollback,因此性能更高
- 信号驱动/O模型
开启套接字信号驱动IO功能,通过系统调用sigaction执行一个信号处理函数。当fd就绪时,就为该进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据
- 异步I/O模型
告知内核启动某个操作,并且在操作完成后通知应用程序。这种模型与信号驱动模型的主要区别是:信号模型时通知应用程序什么时候开启IO,异步IO是通知应用程序IO何时已经完成
多路复用技术
概念:将多个IO阻塞复用到同一个select阻塞上,从而使得单线程的情况下可以处理多个客户端请求。可以理解为(传统领域一个汽车销售对应一个客户,而多路复用则是多个客户注册在同一个平台上,一个销售通过这个平台可以处理多个客户的要求)
优点:相比较与传统的阻塞IO一个线程处理一个客户端请求的模式,多路复用IO在多个客户端请求时,可以实现一个线程既可以处理所有请求,节省了系统创建和维护线程的资源开销,降低了系统的维护工作量
应用场景:
- 服务器需要处理多个socket连接
- 服务端需要处理多种网络协议套接字
linux系统对比:
- select系统支持一个进程打开的fd有限制,由FD_SIZE设置,通常为1024,因此对于那些连接数较高的服务器,不满足需求。虽然可以通过修改宏重新编译内核的方式更改连接限制,但是会带来网络效率的下降。也可以通过多线程的方案改进这个问题,但是多线程之间的共享数据交互会带来系统的复杂性提高以及可维护性降低。epoll没有这个问题,它所支持的fd上限是操作系统的最大文件句柄数,可以通过cat /proc/sys/fs/file- max查看。理论上这个值只与内存有关,1GB支持10万个链接。所以,如果内存无上限,理论上epoll支持的最大文件句柄数也没有上限
- 传统的select/poll有一个致命弱点,在一个socket集合中,因为链路空闲或网络延迟,任意一个时刻可能只有少量的socket链接时活跃的。但是因为select/poll是通过线性扫描的形式来获取活跃的socket链接,因此socket链接数量越多,select/poll效率也就越低。epoll是基于事件通知的方式来获取活跃的socket链接,原理上是通过fd的callback函数实现,理论上只有活跃的socket链接会调用callback函数,因此避免了线性扫描带来的效率问题。
- select、poll、epoll都需fd从内核态到用户态的一个传递,因此如何优化这个过程就成了重中之重。epoll通过mmap(内存映射文件:内核态与用户态通过内存映射的方式公用一块内存地址,避免了内核态拷贝到用户态的过程)加速了这个过程。
epollAPI更简单。包括创建一个epollfd,添加监听事件,阻塞等待所监听的事件发生,关闭epoll描述符等等
IO介绍
BIO
定义:最传统的同步阻塞IO。通过一个acceptor线程监听客户端链接,为每一个客户端链接创建一个线程,通过流的形式进行数据交互。
优点:代码以及思维结构简单
缺点:性能低,因为需要为每一个请求创建一个线程,因此当请求量提高时,线程数会随着1:1的比例线性提升。线程作为虚拟机的宝贵资源,线程越多整个虚拟机的性能越低,最终随着请求量的递增造成堆栈溢出,线程创建失败,最终会导致服务器宕机
因为是一个线程对应一个请求,因此当请求量提高时,只能通过扩展服务器数量以及配置的方式来提升性能。但是如果服务器越来越多,在资金上以及后期维护上都是一个极大的问题。
NIO
定义:非阻塞IO。底层依赖linux提供的select/poll/epoll系统实现。是一种面向块的IO。通过缓冲区,通道、多路复用器等概念,使得服务器端一个线程就可以支持成千上万的客户端请求,与BIO相比提高了一个量级
缓冲区(buffer):与传统IO将数据直接写入流或读取流不同,nio是将数据写入缓冲区或读入缓冲区,任何访问都是通过缓冲区来实现的。缓冲区本质上是一个数组,通常情况下是一个字节数组。但一个缓冲区又不仅仅是一个数组,缓冲区还包含了对数据的结构化访问以及维护读写位置(limit)的功能
- 通道(channel):与传统IO通过单向流的方式进行数据交互不同,nio中通过管道的方式来进行数据的双向交互。也就是说可以进行读写的双向交互
- 多路复用器(selector):多路复用器作为NIO中最重要的概念,它的本质作用是获取处于就绪状态的socket链接。nio的架构中,客户端与服务端建立链接后会在服务端的selector上注册一个channel通道用于读写操作。当通道发生读写操作时,通道就会处于就绪状态。selector通过顺序检索(select/poll)或事件通知(epoll)的方式获取继续的channel集合,返回给应用程序进行后续的操作。因为JDK底层是通过select系统实现的selector,因此没有最大连接数的限制,进一步的提升了nio的使用效率
AIO
定义:真正的异步非阻塞IO。底层依赖linux系统信号驱动IO模型来实现。不需要selector轮询的方式获取就绪的channel,进一步简化了nio的编程模型总结
|
| 同步阻塞IO(BIO) | 伪异步IO | 非阻塞IO(NIO) | 异步IO(AIO) | | —- | —- | —- | —- | —- | | 客户端个数:IO线程数 | 1:1 | M:N(M可以>N) | M:1 | M:0 | | IO阻塞类型 | 阻塞 | 阻塞 | 非阻塞 | 非阻塞 | | IO同步类型 | 同步 | 同步 | 同步 | 异步 | | API使用难度 | 简单 | 简单 | 非常复杂 | 复杂 | | 调试难度 | 简单 | 简单 | 复杂 | 复杂 | | 可靠性 | 非常差 | 差 | 高 | 高 | | 吞吐量 | 低 | 中 | 高 | 高 |