1、jdk1.4之前(jdk1.4开始提供了nio)的早起版本,java对I/O的支持并不完善,开发人员开发高性能I/O程序时,面临的问题主要有:
没有缓冲区,I/O性能存在问题
没有Channel概念,只有输入输出流
同步阻塞式I/O通信(BIO),通常会导致通信线程被长时间阻塞
支持字符集有限,硬件移植性不好
2、linux的内核将所有外部设备都看做一个文件来操作,对一个文件的读写操作会调用内核提供的系统命令,返回一个file descriptor(fd,文件描述符)。而对一个socket的读写也会有响相应的描述符,称为 socketfd(socket描述符),描述符就是一个数字,它指向内核中的一个结构体(文件路径,数据区等一些属性)。根据unix网络编程对I/O模型的分类,unix提供了5种I/O模型,分别如下:
(1)阻塞I/O模型:最常用的I/O模型就是阻塞I/O模型,缺省情况下,所有文件操作都是阻塞的。我们以套接字接口为例来讲解此模型:在进程空间中调用recvfrom,其系统调用知道数据包到达且被复制到应 用进程的缓冲区中或者发生错误时才返回,在此期间一直会等待,进程在调用recvfrom开始到它返回的整段时间内都是被阻塞的,因为被称为阻塞I/O模型。
(2)非阻塞I/O模型:recvfrom从应用层到内核的时候,如果该缓冲区没有数据的话,就直接返回一个EWOULDBLOCK错误,一般都是对非阻塞I/O模型进行轮询检查这个状态,看内核是不是有数据到来。
(3)I/O复用模型:linux提供了select/poll,进程通过将一个或多个fd传递给select或poll系统调用,阻塞在select操作上,这样select/poll可以帮我们侦测多个fd是否处于就绪状态。select/poll是顺序扫描 fd是否就绪,而且支持的fd数量有限,因此它的使用受到了一些制约。select的最大缺陷是单个进程所打开的fd是有一定限制的,默认值为1024。linux还提供了一个epoll系统调用,epoll使用基于事件驱动方式代 替顺序扫描,因此性能更高。当有fd就绪时,立即回调函数callback。epoll支持的fd上限是操作系统的最大文件句柄数。
(4)信号驱动I/O模型:首先开启套接口信号驱动I/O功能,并通过系统调用sigaction执行一个信号处理函数(此系统调用立即返回,进程继续工作,它是非阻塞的)。当数据准备就绪时,就为该进程生成一 个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据,并通知主循环函数处理数据。
(5)异步I/O:告知内核启动某个操作,并让内核在整个操作完成后(包括将数据从内核复制到用户自己的缓冲区)通知我们。这种模型与信号驱动模型的主要区别是:信号驱动I/O由内核通知我们何时可以开 始一个I/O操作;异步I/O模型由内核通知我们I/O操作何时已经完成。
3、epoll在select基础上的改进
(1)支持一个进程打开的socket描述符(fd)不受限制(仅受限于操作系统的最大文件句柄数)。select支持的fd默认值是1024。
(2)I/O效率不会随着fd数目的增加而线性下降
(3)使用mmap加速内核与用户空间的消息传递
(4)api更加简单
4、nio类库的相关概念
(1)缓冲区Buffer
在面向流的I/O中,可以将数据直接写入或者将数据直接读到 stream对象中。在nio库中,所有数据都是用缓冲区处理的。在读取数据时,它是直接读到缓冲区中,在写入数据时,写入到缓冲区中。任何时候 访问nio中的数据,都是通过缓冲区进行操作。缓冲区实质上是一个数组,同时缓冲区还提供了对数据的结构化访问以及维护读写位置(limit)等信息。最常用的缓冲区是ByteBuffer,java每一种基本类型 (Boolean除外)都对应一种缓冲区。
(2)通道Channel
Channel是一个通道,可以通过它读取和写入数据,它就像自来水管一样,网络数据通过Channel读取或写入。通道与流的不同之处在于通道是双向的,流只是在一个方向上的移动,而且通道可以用于读、写 或同时读写。Channel可以分为两大类:分别是用于网络读写的SelectableChannel和用于文件操作的FileChannel。
(3)多路复用器Selector
Selector会不断的轮询注册在其上的Channel,如果某个Channel上面有新的TCP连接接入、读或写事件,这个Channel就处于就绪状态,会被Selector轮询出来,然后通过SelectionKey可以获取就绪的 Channel的集合,进行后续的I/O操作。一个多路复用器Selector可以同时轮询多个Channel。