文章目录
IO的发展过程
网络编程的基础模型是Client/Server模型,也就是两个进程之间相互通信,其中服务端提供位置信息(绑定的IP地址和监听端口),客户端通过连接操作向服务端监听的地址发起连接请求,通过三次握手建立连接,如果连接建立成功,双方就可以通过网络套接字进行通信。
第一阶段 同步阻塞通信 BIO
由一个独立的Acceptor线程复负责监听客户端的连接,它接收到客户端请求后为每个客户端创建一个新的线程进行链路处理,处理完成后,通过输出流返回应答给客户端,线程销毁。
缺点:
- 当线程数膨胀后,系统的性能下降
- 随着并发访问量的继续增大,系统会发生堆栈溢出、创建新线程失败等问题,最终导致进程宕机或者僵死,不能对外提供服务
- 缺乏弹性伸缩能力
第二阶段 伪异步IO通信
当有新的客户端接入时,将客户端的Socket封装成一个Task投递到后端的线程池中进行处理,由于线程池可以设置消息队列的大小和最大线程数,因此,它的资源占用是可控的,无论多少个客户端并发访问,都不会导致资源的耗尽和宕机。
缺点:
- 底层采用的同步IO阻塞模型,输入流和输出流的读和写操作都是同步组塞的,阻塞的时间取决于客户端I/O线程的处理熟读和网络I/O的传输速度
- 如果客户端反返回应答时间过长,将引起级联故障
- 当线程池的任务队列满了之后,Acceptor线程也被阻塞了,新得客户端请求信息将被拒绝,客户端会发生大量的连接超时
第三阶段 同步非阻塞通信 NIO
提供了高速的、面向块的I/O。Selector底层使用epoll完成对I/O的多路复用。
第四阶段 异步通信 AIO
NIO 2.0引入了新的异步通道的概念,提供了异步文件通道和异步套接字通道的实现。
IO
Java IO类库的框架
虽然java IO类库庞大,但总体来说其框架还是很清楚的。从是读媒介还是写媒介的维度看,Java IO可以分为:
字节流 | 字符流 | |
---|---|---|
输入流 | InputStream | Reader |
输出流 | OutputStream | Writer |
在我们的实际应用中,我们用到的一般是它们的子类,之所以设计这么多子类,目的就是让每一个类都负责不同的功能,以方便我们开发各种应用。各类用途汇总如下:
- 文件访问
- 网络访问
- 内存缓存访问
- 线程内部通信(管道)
- 缓冲
- 过滤
- 解析
- 读写文本 (Readers / Writers)
- 读写基本类型数据 (long, int etc.)
- 读写对象
Java IO:管道媒介
管道主要用来实现同一个虚拟机中的两个线程进行交流。因此,一个管道既可以作为数据源媒介也可作为目标媒介。需要注意的是java中的管道和Unix/Linux中的管道含义并不一样,在Unix/Linux中管道可以作为两个位于不同空间进程通信的媒介,而在java中,管道只能为同一个JVM进程中的不同线程进行通信。和管道相关的IO类为:PipedInputStream和PipedOutputStream。
Java IO:网络媒介
关于Java IO面向网络媒介的操作即Java 网络编程,其核心是Socket。
BufferedInputStream和BufferedOutputStream
BufferedInputStream顾名思义,就是在对流进行写入时提供一个buffer来提高IO效率。在进行磁盘或网络IO时,原始的InputStream对数据读取的过程都是一个字节一个字节操作的,而BufferedInputStream在其内部提供了一个buffer,在读数据时,会一次读取一大块数据到buffer中,这样比单字节的操作效率要高的多,特别是进程磁盘IO和对大量数据进行读写的时候。
使用BufferedInputStream十分简单,只要把普通的输入流和BufferedInputStream组合到一起即可。
BufferedReader、BufferedWriter 的作用基本和BufferedInputStream、BufferedOutputStream一致,具体用法和原理都差不多 ,只不过一个是面向字符流一个是面向字节流。
NIO
***NIO***即New IO,这个库是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多。
在Java API中提供了两套NIO,一套是针对标准输入输出NIO,另一套就是网络编程NIO。
NIO基础
Buffer和Channel是标准NIO中的核心对象(网络NIO中还有个Selector核心对象),几乎每一个IO操作中都会用到它们。
Channel是对原IO中流的模拟,任何来源和目的数据都必须通过一个Channel对象。一个Buffer实质上是一个容器对象,发给Channel的所有对象都必须先放到Buffer中;同样的,从Channel中读取的任何数据都要读到Buffer中。
buffer
Buffer是一个对象,它包含一些要写入或读出的数据。在NIO中,数据是放入buffer对象的,而在IO中,数据是直接写入或者读到Stream对象的。应用程序不能直接对 Channel 进行读写操作,而必须通过 Buffer 来进行,即 Channel 是通过 Buffer 来读写数据的。
在NIO中,所有的数据都是用Buffer处理的,它是NIO读写数据的中转池。Buffer实质上是一个数组,通常是一个字节数据,但也可以是其他类型的数组。但一个缓冲区不仅仅是一个数组,重要的是它提供了对数据的结构化访问,而且还可以跟踪系统的读写进程。
使用 Buffer 读写数据一般遵循以下四个步骤:
- 写入数据到 Buffer;
- 调用 flip() 方法;
- 从 Buffer 中读取数据;
- 调用 clear() 方法或者 compact() 方法。
当向 Buffer 写入数据时,Buffer 会记录下写了多少数据。一旦要读取数据,需要通过 flip() 方法将 Buffer 从写模式切换到读模式。在读模式下,可以读取之前写入到 Buffer 的所有数据。
一旦读完了所有的数据,就需要清空缓冲区,让它可以再次被写入。有两种方式能清空缓冲区:调用 clear() 或 compact() 方法。clear() 方法会清空整个缓冲区。compact() 方法只会清除已经读过的数据。任何未读的数据都被移到缓冲区的起始处,新写入的数据将放到缓冲区未读数据的后面。
这些Buffer覆盖了你能通过IO发送的基本数据类型:byte, short, int, long, float, double 和char。 Mappedyteuffer,用于表示内存映射文件。
channel
Channel是一个对象,可以通过它读取和写入数据。可以把它看做IO中的流。但是它和流相比还有一些不同:
- Channel是双向的,既可以读又可以写,而流是单向的
- Channel可以进行异步的读写
- 对Channel的读写必须通过buffer对象
正如上面提到的,所有数据都通过Buffer对象处理,所以,永远不会将字节直接写入到Channel中,相反,是将数据写入到Buffer中;同样,也不会从Channel中读取字节,而是将数据从Channel读入Buffer,再从Buffer获取这个字节。
因为Channel是双向的,所以Channel可以比流更好地反映出底层操作系统的真实情况。特别是在Unix模型中,底层操作系统通常都是双向的。
在Java NIO中Channel主要有如下几种类型:
- FileChannel:从文件读取数据的
- DatagramChannel:读写UDP网络协议数据
- SocketChannel:读写TCP网络协议数据
- ServerSocketChannel:可以监听TCP连接
Selector
Selector是一个对象,它可以注册到很多个Channel上,监听各个Channel上发生的事件,并且能够根据事件情况决定Channel读写。这样,通过一个线程管理多个Channel,就可以处理大量网络连接了,在linux等主流操作系统上通过epoll实现,它没有连接句柄数的限制,这意味着一个Selector线程可以同时处理成千上万个客户端连接,而且性能不会随着客户端的增加而线性下降
AIO
NIO 2.0引入了新的异步通道的概念,并提供了异步文件通道和异步套接字通道的实现,异步通道提供以下两种方式获取操作结果:
-
通过java.util.concurrent.Future类来表示异步操作的结果
-
在执行异步操作的时候传入一个java.nio.channels
CompletionHandler接口的实现类作为操作完成的回调
NIO2.0的异步套接字通道是真正的异步非组赛I/O,对应于UNIX网络编程中的事件驱动I/O(AIO)。它不需要多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写。