1.什么是IO?
1.1 什么是流?
IO在本质上是单个字节的移动,而流可以说是字节移动的载体和方式,它不停的向目标处移动数据,我们要做的就是根据流的方向从流中读取数据或者向流中写入数据。最简单的Java流的例子就是下载电影,肯定不是等电影全部下载在内存中再保存到磁盘上,本质上是下载一个字节就保存一个字节。
一个流,必有源和目标,它们可以是计算机内存的某些区域,也可以是磁盘文件,甚至可以是Internet上的某个URL。流的方向是重要的,根据流的方向,流可分为两类:输入流和输出流。我们从输入流读取数据,向输出流写入数据。
Java传统的io是基于流的io即BIO(同步阻塞IO),从jdk1.4开始提供基于块的io,即nio即同步非阻塞IO,到jdk1.7之后出现aio 异步非阻塞IO。
1.2 IO分类
Java对io的支持主要集中在io包下,显然可以分为下面两类:
-
基于字节操作的io接口:InputStream 和 OutputStream
-
基于字符操作的io接口:Writer 和 Reader
不管磁盘还是网络传输,最小的存储单位都是字节,但是程序中操作的数据大多都是字符形式的,所以Java也提供了字符型的流。流并不等于io,还有很重要的一点,数据的传输方式,也就是数据写到哪里的问题,主要是以下两种:
-
基于磁盘操作的io接口:File
-
基于网络操作的io接口:Socket
到jdk1.4以后 出现socketChannel 管道,支持 select poll epoll等通讯方式。
1.3 磁盘IO和网络IO工作机制
1.3.1 磁盘IO-file
io中数据写到何处也是重要的一点,其中最主要的就是将数据持久化到磁盘。数据在磁盘上最小的描述就是文件,上层应用对磁盘的读和写都是针对文件而言的在java中,以File类来表示文件,如:
File file = new File("D:/test.txt");
但是严格来说,File并不表示一个真实的存在于磁盘上的文件。就像上面代码的文件其实并不存在,File做的只是根据你所提供的文件描述符,返回某一路径的虚拟对象,它并不关心文件或路径是否存在,可能存在,也可能是捏造的。就好象一张名片,名片的背后代表的是人。为什么要这么设计?
以FileInputStream读取文件为例,过程是这样的:当传入一个文件路径时,会根据这个路径创建File对象,作为这个文件的一个“名片”。当我们试图通过FileInputStream对象去操作文件的时候,将会真正创建一个关联真实存在的磁盘文件的文件描述符FileDescriptor,通过FileInputStream构造方法可以看出:
fd = new FileDescriptor();
如果说File是文件的名片,那么FileDescriptor就是真正指向了一个打开的文件,可以操作磁盘文件。例如FileDescriptor.sync()方法可以将缓存中的数据强制刷新到磁盘文件中。如果我们需要读取的是字符,还需要通过StreamDecoder类将字节解码成字符。至于如何从物理磁盘上读取数据,那就是操作系统做的事情了。
1.3.2 网络io-socket
大部分情况下我们使用的都是基于TCP/IP协议的流Socket,因为它是一种稳定的通信协议。Socket就像一个插座,计算机通过Socket就能和网络或者其他计算机上进行通讯;当有数据通讯的需求时,只需要建立一个Socket“插座”,通过网卡与其他计算机相连获取数据。
Socket位于传输层和应用层之间,向应用层统一提供编程接口,应用层不必知道传输层的协议细节。Java中对Socket的支持主要是以下两种:
(1)基于TCP的Socket:提供给应用层可靠的流式数据服务,使用TCP的Socket应用程序协议:BGP,HTTP,FTP,TELNET等。优点:基于数据传输的可靠性。
(2)基于UDP的Socket:适用于数据传输可靠性要求不高的场合。基于UDP的Socket应用程序协议:RIP,SNMP,L2TP等。
面向连接的套接字的系统调用时序图:
1.4 为什么要使用NIO?
现在互联网提供的在线服务都是走网络I/O,那么对于网络I/O,传统的阻塞式I/O,一个线程对应一个连接,采用线程池的模式在大部分场景下简单高效。当有大量的连接的时候,我们可以为每一个连接建立一个线程来操作。但是这种做法带来的缺陷也是显而易见的:
-
硬件能够支持大量的并发。
-
并发的数量始终有一个上限。
-
各个线程之间的优先级不好控制。
-
各个Client之间的交互与同步困难。
我们也可以使用一个线程来处理所有的请求,使用不阻塞的IO,轮询查询所有的Client。这种做法同样也有缺陷:无法迅速响应Client端,同时会消耗大量轮询查询的时间。所以,我们需要一种poll的模式来处理这种情况,从大量的网络连接中找出来真正需要服务的Client。这正是NIO诞生的原因:提供一种Poll的模式,在所有的Client中找到需要服务的Client,现在支持epoll。JDK中,有一个非常有意思的库:NIO(New I/O)。这个库中有3个重要的类,分别是java.nio.channels中Selector和Channel,以及java.nio中的Buffer。
1.Channel代表一个可以被用于Poll操作的对象(可以是文件流/网络流),Channel能够被注册到一Selector中。
2.通过调用Selector的select方法可以从所有的Channel中找到需要服务的实例(Accept,read ..)。
3.Buffer对象提供读写数据的缓存。对于我们熟悉的Stream对象,Buffer提供更好的性能以及更好的编程透明性(人为控制缓存的大小以及具体的操作)。
2.BIO NIO AIO是什么,有什么特点?
这里主要讨论的是网络IO:
1.首先需要提到BIO中socket,这里面的阻塞点 accept 等待连接 ,read/write数据.
2.在说说NIO是中socketChannel accept不会阻塞,而是交给一个线程去接收所有连接,将所有连接放到一个缓存队列上;最终会通过selector(可以看做是监听器)监听所有客户端的连接数据请求,会调用doselect()方法(阻塞点) 通知分发到(dispatch)到真正操作数据读写的业务线程池里,进行数据处理。
看看dubbo rpc的io处理机制:
3.阻塞/非阻塞,同步/异步 ?
3.1 阻塞(blocking)与非阻塞(non-blocking)IO
IO的阻塞、非阻塞主要表现在一个IO操作过程中,如果有些操作很慢,比如读操作时需要准备数据,那么当前IO进程是否等待操作完成,还是得知暂时不能操作后先去做别的事情?一直等待下去,什么事也不做直到完成,这就是阻塞。抽空做些别的事情,这是非阻塞。
非阻塞IO会在发出IO请求后立即得到回应,即使数据包没有准备好,也会返回一个错误标识,使得操作进程不会阻塞在那里。操作进程会通过多次请求的方式直到数据准备好,返回成功的标识。
3.2同步(synchronous)与异步(asynchronous)IO
先来看看正式点的定义,POSIX标准将IO模型分为了两种:同步IO和异步IO,Richard Stevens在《Unix网络编程卷》中也总结道:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operationcompletes;An asynchronous I/O operation does not cause the requesting process to be blocked;
可以看出,判断同步和异步的标准在于:一个IO操作直到完成,是否导致程序进程的阻塞。如果阻塞就是同步的,没有阻塞就是异步的。这里的IO操作指的是真实的IO操作,也就是数据从内核拷贝到系统进程(读)的过程。
参考:
2.IO阻塞与非阻塞