若是再有人问你 Java IO,就把这篇文章砸他头上!

1、简介

说到 I/O,想必你们都不会陌生, I/O 英语全称:Input/Output,即 输入/输出,一般 指数据在内部存储器和外部存储器或其余周边设备之间的输入和输出

好比咱们经常使用的SD 卡U 盘移动硬盘等等存储文件的硬件设备,当咱们将其插入电脑的 usb 硬件接口时,咱们就能够从电脑中读取设备中的信息或者写入信息,这个过程就涉及到 I/O 的操做。java

若是有人再问你 Java IO,就把这篇文章砸他头上

固然,涉及 I/O 的操做,不只仅局限于硬件设备的读写,还要网络数据的传输,好比,咱们在电脑上用浏览器搜索互联网上的信息,这个过程也涉及到 I/O 的操做。程序员

若是有人再问你 Java IO,就把这篇文章砸他头上

不管是从磁盘中读写文件,仍是在网络中传输数据,能够说 I/O 主要为处理人机交互机与机交互中获取和交换信息提供的一套解决方案。编程

在 Java 的 IO 体系中,类将近有 80 个,位于java.io包下,感受很复杂,可是这些类大体能够分红四组:后端

  • 基于字节操做的 I/O 接口:InputStream 和 OutputStream
  • 基于字符操做的 I/O 接口:Writer 和 Reader
  • 基于磁盘操做的 I/O 接口:File
  • 基于网络操做的 I/O 接口:Socket

前两组主要从传输数据的数据格式不一样,进行分组;后两组主要从传输数据的方式不一样,进行分组。浏览器

虽然 Socket 类并不在java.io包下,可是咱们仍然把它们划分在一块儿,由于 I/O 的核心问题,要么是数据格式影响 I/O 操做,要么是传输方式影响 I/O 操做,也就是将什么样的数据写到什么地方的问题,I/O 只是人与机器或者机器与机器交互的手段,除了在它们可以完成这个交互功能外,咱们关注的就是如何提升它的运行效率了,而数据格式传输方式是影响效率最关键的因素。缓存

本文后面,也是基于这两个点进行深刻展开分析。服务器

2、基于字节操做的接口

基于字节的输入和输出操做接口分别是:InputStream 和 OutputStream 。网络

2.一、字节输入流

InputStream 输入流的类继承层次以下图所示:数据结构

若是有人再问你 Java IO,就把这篇文章砸他头上

输入流根据数据节点类型和处理方式,分别能够划分出了若干个子类,以下图:多线程

若是有人再问你 Java IO,就把这篇文章砸他头上

OutputStream 输出流的类层次结构也是相似。

2.二、字节输出流

OutputStream 输出流的类继承层次以下图所示:

若是有人再问你 Java IO,就把这篇文章砸他头上

输出流根据数据节点类型和处理方式,也分别能够划分出了若干个子类,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

在这里就不详细的介绍各个子类的使用方法,有兴趣的朋友能够查看 JDK 的 API 说明文档,笔者也会在后期的文章会进行详细的介绍,这里只是重点想说一下,不管是输入仍是输出,操做数据的方式能够组合使用,各个处理流的类并非只操做固定的节点流,好比以下输出方式:

//将文件输出流包装到序列化输出流中,再将序列化输出流包装到缓冲中OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream(new File("fileName")));

另外,输出流最终写到什么地方必需要指定,要么是写到硬盘中,要么是写到网络中,从图中能够发现,写网络实际上也是写文件,只不过写到网络中,须要通过底层操做系统将数据发送到其余的计算机中,而不是写入到本地硬盘中。

3、基于字符操做的接口

无论是磁盘仍是网络传输,最小的存储单元都是字节,而不是字符,因此 I/O 操做的都是字节而不是字符,可是为何要有操做字符的 I/O 接口呢?

这是由于咱们的程序中一般操做的数据都是以字符形式,为了程序操做更方便而提供一个直接写字符的 I/O 接口,仅此而已。

基于字符的输入和输出操做接口分别是:Reader 和 Writer ,下图是字符的 I/O 操做接口涉及到的类结构图。

3.一、字符输入流

Reader 输入流的类继承层次以下图所示:

若是有人再问你 Java IO,就把这篇文章砸他头上

一样的,输入流根据数据节点类型和处理方式,分别能够划分出了若干个子类,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

3.二、字符输出流

Writer 输出流的类继承层次以下图所示:

若是有人再问你 Java IO,就把这篇文章砸他头上

一样的,输出流根据数据节点类型和处理方式分类,分别能够划分出了若干个子类,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

无论是 Reader 仍是 Writer 类,它们都只定义了读取或写入数据字符的方式,也就是说要么是读要么是写,可是并无规定数据要写到哪去,写到哪去就是咱们后面要讨论的基于磁盘或网络的工做机制。

4、字节与字符的转化

刚刚咱们说到,无论是磁盘仍是网络传输,最小的存储单元都是字节,而不是字符,设计字符的缘由是为了程序操做更方便,那么怎么将字符转化成字节或者将字节转化成字符呢?

InputStreamReader 和 OutputStreamWriter 就是转化桥梁。

4.一、输入流转化过程

输入流字符解码相关类结构的转化过程以下图所示:

若是有人再问你 Java IO,就把这篇文章砸他头上

从图上能够看到,InputStreamReader 类是字节到字符的转化桥梁, 其中StreamDecoder指的是一个解码操做类,Charset指的是字符集。

InputStream 到 Reader 的过程须要指定编码字符集,不然将采用操做系统默认字符集,极可能会出现乱码问题,StreamDecoder 则是完成字节到字符的解码的实现类。

打开源码部分,InputStream 到 Reader 转化过程,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

4.一、输出流转化过程

输出流转化过程也是相似,以下图所示:

若是有人再问你 Java IO,就把这篇文章砸他头上

经过 OutputStreamWriter 类完成字符到字节的编码过程,由 StreamEncoder 完成编码过程。

源码部分,Writer 到 OutputStream 转化过程,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

5、基于磁盘操做的接口

前面介绍了 Java I/O 的操做接口,这些接口主要定义了如何操做数据,以及介绍了操做数据格式的方式:字节流和字符流。

还有一个关键问题就是数据写到何处,其中一个主要的处理方式就是将数据持久化到物理磁盘。

咱们知道数据在磁盘的惟一最小描述就是文件,也就是说上层应用程序只能经过文件来操做磁盘上的数据,文件也是操做系统和磁盘驱动器交互的一个最小单元。

若是有人再问你 Java IO,就把这篇文章砸他头上

在 Java I/O 体系中,File 类是惟一表明磁盘文件自己的对象

File 类定义了一些与平台无关的方法来操做文件,包括检查一个文件是否存在、建立、删除文件、重命名文件、判断文件的读写权限是否存在、设置和查询文件的最近修改时间等等操做。

值得注意的是 Java 中一般的 File 并不表明一个真实存在的文件对象,当你经过指定一个路径描述符时,它就会返回一个表明这个路径相关联的一个虚拟对象,这个多是一个真实存在的文件或者是一个包含多个文件的目录。

例如,读取一个文件内容,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

以上面的程序为例,从硬盘中读取一段文本字符,操做流程以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

咱们再来看看源码执行流程。

当咱们传入一个指定的文件名来建立 File 对象,经过 FileReader 来读取文件内容时,会自动建立一个FileInputStream对象来读取文件内容,也就是咱们上文中所说的字节流来读取文件。

若是有人再问你 Java IO,就把这篇文章砸他头上

紧接着,会建立一个FileDescriptor的对象,其实这个对象就是真正表明一个存在的文件对象的描述。能够经过FileInputStream对象调用getFD()方法获取真正与底层操做系统关联的文件描述。

若是有人再问你 Java IO,就把这篇文章砸他头上

因为咱们须要读取的是字符格式,因此须要 StreamDecoder 类将byte解码为char格式,至于如何从磁盘驱动器上读取一段数据,由操做系统帮咱们完成。

6、基于网络操做的接口

继续来讲说数据写到何处的另外一种处理方式:将数据写入互联网中以供其余电脑能访问

6.一、Socket 简介

在现实中,Socket 这个概念没有一个具体的实体,它是描述计算机之间完成相互通讯一种抽象定义。

打个比方,能够把 Socket 比做为两个城市之间的交通工具,有了它,就能够在城市之间来回穿梭了。而且,交通工具备多种,每种交通工具也有相应的交通规则。Socket 也同样,也有多种。大部分状况下咱们使用的都是基于 TCP/IP 的流套接字,它是一种稳定的通讯协议。

典型的基于 Socket 通讯的应用程序场景,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

主机 A 的应用程序要想和主机 B 的应用程序通讯,必须经过 Socket 创建链接,而创建 Socket 链接必须须要底层 TCP/IP 协议来创建 TCP 链接。

6.二、创建通讯链路

咱们知道网络层使用的 IP 协议能够帮助咱们根据 IP 地址来找到目标主机,可是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通讯就要经过 TCP 或 UPD 的地址也就是端口号来指定。这样就能够经过一个 Socket 实例表明惟一一个主机上的一个应用程序的通讯链路了。

为了准确无误地把数据送达目标处,TCP 协议采用了三次握手策略,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

其中,SYN 全称为 Synchronize Sequence Numbers,表示同步序列编号,是 TCP/IP 创建链接时使用的握手信号。

ACK 全称为 Acknowledge character,即确认字符,表示发来的数据已确认接收无误

在客户机和服务器之间创建正常的 TCP 网络链接时,客户机首先发出一个SYN 消息,服务器使用SYN + ACK应答表示接收到了这个消息,最后客户机再以ACK消息响应。

这样在客户机和服务器之间才能创建起可靠的 TCP 链接,数据才能够在客户机和服务器之间传递。

简单流程以下:

  • 发送端 –(发送带有 SYN 标志的数据包 )–> 接受端(第一次握手);
  • 接受端 –(发送带有 SYN + ACK 标志的数据包)–> 发送端(第二次握手);
  • 发送端 –(发送带有 ACK 标志的数据包) –> 接受端(第三次握手);

完成三次握手以后,客户端应用程序与服务器应用程序就能够开始传送数据了。

传输数据是咱们创建链接的主要目的,如何经过 Socket 传输数据呢?

6.三、传输数据

当客户端要与服务端通讯时,客户端首先要建立一个 Socket 实例,默认操做系统将为这个 Socket 实例分配一个没有被使用的本地端口号,并建立一个包含本地、远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个链接关闭。

若是有人再问你 Java IO,就把这篇文章砸他头上

与之对应的服务端,也将建立一个 ServerSocket 实例,ServerSocket 建立比较简单,只要指定的端口号没有被占用,通常实例建立都会成功,同时操做系统也会为 ServerSocket 实例建立一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听地址的通配符,一般状况下都是*即监听全部地址。

以后当调用 accept() 方法时,将进入阻塞状态,等待客户端的请求。

若是有人再问你 Java IO,就把这篇文章砸他头上

咱们先启动服务端程序,再运行客户端,服务端收到客户端发送的信息,服务端打印结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

注意,客户端只有与服务端创建三次握手成功以后,才会发送数据,而 TCP/IP 握手过程,底层操做系统已经帮咱们实现了!

当链接已经创建成功,服务端和客户端都会拥有一个 Socket 实例,每一个 Socket 实例都有一个InputStreamOutputStream,正如咱们前面所说的,网络 I/O 都是以字节流传输的,Socket 正是经过这两个对象来交换数据。

当 Socket 对象建立时,操做系统将会为 InputStream 和 OutputStream 分别分配必定大小的缓冲区,数据的写入和读取都是经过这个缓存区完成的。

写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据将被发送到另外一端 InputStream 的 RecvQ 队列中,若是这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据。

值得特别注意的是,缓存区的大小以及写入端的速度和读取端的速度很是影响这个链接的数据传输效率,因为可能会发生阻塞,因此网络 I/O 与磁盘 I/O 在数据的写入和读取还要有一个协调的过程,若是两边同时传送数据时可能会产生死锁的问题。

如何提升网络 IO 传输效率、保证数据传输的可靠,已经成了工程师们急需解决的问题。

6.四、IO 工做方式

在计算机中,IO 传输数据有三种工做方式,分别是BIO、NIO、AIO

在讲解BIO、NIO、AIO以前,咱们先来回顾一下这几个概念:同步与异步,阻塞与非阻塞

同步与异步的区别

  • 同步就是发起一个请求后,接受者未处理完请求以前,不返回结果。
  • 异步就是发起一个请求后,马上获得接受者的回应表示已接收到请求,可是接受者并无处理完,接受者一般依靠事件回调等机制来通知请求者其处理结果。

阻塞和非阻塞的区别

  • 阻塞就是请求者发起一个请求,一直等待其请求结果返回,也就是当前线程会被挂起,没法从事其余任务,只有当条件就绪才能继续。
  • 非阻塞就是请求者发起一个请求,不用一直等着结果返回,能够先去干其余事情,当条件就绪的时候,就自动回来。

而咱们要讲的BIO、NIO、AIO就是同步与异步、阻塞与非阻塞的组合。

  • BIO:同步阻塞 IO;
  • NIO:同步非阻塞 IO;
  • AIO:异步非阻塞 IO;

6.4.一、BIO

BIO 俗称同步阻塞 IO,一种很是传统的 IO 模型,好比咱们上面所举的那个程序例子,就是一个典型的**同步阻塞 IO **的工做方式。

若是有人再问你 Java IO,就把这篇文章砸他头上

采用 BIO 通讯模型的服务端,一般由一个独立的Acceptor线程负责监听客户端的链接。

咱们通常在服务端经过while(true)循环中会调用accept()方法等待监听客户端的链接,一旦接收到一个链接请求,就能够创建通讯套接字进行读写操做,此时不能再接收其余客户端链接请求,只能等待同当前链接的客户端的操做执行完成, 不过能够经过多线程来支持多个客户端的链接。

客户端多线程操做,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

服务端多线程操做,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

服务端运行结果,以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

若是要让 BIO 通讯模型可以同时处理多个客户端请求,就必须使用多线程,也就是说它在接收到客户端链接请求以后为每一个客户端建立一个新的线程进行链路处理,处理完成以后,经过输出流返回应答给客户端,线程销毁。

这就是典型的一请求一应答通讯模型 。

若是出现 100、1000、甚至 10000 个用户同时访问服务器,这个时候,若是使用这种模型,那么服务端也会建立与之相同的线程数量,线程数急剧膨胀可能会致使线程堆栈溢出、建立新线程失败等问题,最终致使进程宕机或者僵死,不能对外提供服务

固然,咱们能够经过使用 Java 中 ThreadPoolExecutor 线程池机制来改善,让线程的建立和回收成本相对较低,保证了系统有限的资源的控制,实现了 N (客户端请求数量)大于 M (处理客户端请求的线程数量)的伪异步 I/O 模型。

6.4.二、伪异步 BIO

为了解决同步阻塞 I/O 面临的一个链路须要一个线程处理的问题,后来有人对它的线程模型进行了优化,后端经过一个线程池来处理多个客户端的请求接入,造成客户端个数 M:线程池最大线程数 N 的比例关系,其中 M 能够远远大于 N,经过线程池能够灵活地调配线程资源,设置线程的最大值,防止因为海量并发接入致使资源耗尽。

伪异步 IO 模型图,以下图:

若是有人再问你 Java IO,就把这篇文章砸他头上

采用线程池和任务队列能够实现一种叫作伪异步的 I/O 通讯框架,当有新的客户端接入时,将客户端的 Socket 封装成一个 Task 投递到后端的线程池中进行处理。

Java 的线程池维护一个消息队列和 N 个活跃线程,对消息队列中的任务进行处理。

客户端,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

服务端,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

先启动服务端程序,再启动客户端程序,看看运行结果!

服务端,运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

客户端,运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

本例中测试的客户端数量是 30,服务端使用 java 线程池来处理任务,线程数量为 5 个,服务端不用为每一个客户端都建立一个线程,因为线程池能够设置消息队列的大小和最大线程数,所以,它的资源占用是可控的,不管多少个客户端并发访问,都不会致使资源的耗尽和宕机。

在活动链接数不是特别高的状况下,这种模型是还不错,可让每个链接专一于本身的 I/O 而且编程模型简单,也不用过多考虑系统的过载、限流等问题。

可是,它的底层仍然是同步阻塞的 BIO 模型,当面对十万甚至百万级链接的时候,传统的 BIO 模型真的是无能为力的,咱们须要一种更高效的 I/O 处理模型来应对更高的并发量。

6.4.三、NIO

NIO 中的 N 能够理解为Non-blocking,一种同步非阻塞的 I/O 模型,在 Java 1.4 中引入,对应的在java.nio包下。

NIO 新增了Channel、Selector、Buffer等抽象概念,支持面向缓冲、基于通道的 I/O 操做方法。

NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel和 ServerSocketChannel 两种不一样的套接字通道实现。

NIO 这两种通道都支持阻塞和非阻塞两种模式。阻塞模式使用就像传统中的支持同样,比较简单,可是性能和可靠性都很差;非阻塞模式正好与之相反

对于低负载、低并发的应用程序,可使用同步阻塞 I/O 来提高开发效率和更好的维护性;对于高负载、高并发的(网络)应用,应使用 NIO 的非阻塞模式来开发。

咱们先看一下 NIO 涉及到的核心关联类图,以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

上图中有三个关键类:Channel 、Selector 和 Buffer,它们是 NIO 中的核心概念。

  • Channel:能够理解为通道;
  • Selector:能够理解为选择器;
  • Buffer:能够理解为数据缓冲流;

咱们仍是用前面的城市交通工具来继续形容 NIO 的工做方式,这里的Channel要比Socket更加具体,它能够比做为某种具体的交通工具,如汽车或是高铁、飞机等,而Selector能够比做为一个车站的车辆运行调度系统,它将负责监控每辆车的当前运行状态:是已经出站仍是在路上等等,也就是说它能够轮询每一个Channel的状态。

还有一个Buffer类,你能够将它看做为 IO 中Stream,可是它比 IO 中的Stream更加具体化,咱们能够将它比做为车上的座位,Channel若是是汽车的话,那么Buffer就是汽车上的座位,Channel若是是高铁上,那么Buffer就是高铁上的座位,它始终是一个具体的概念,这一点与Stream不一样。

Socket 中的 Stream只能表明是一个座位,至因而什么座位由你本身去想象,也就是说你在上车以前并不知道这个车上是否还有没有座位,也不知道上的是什么车,由于你并不能选择,这些信息都已经被封装在了运输工具(Socket)里面了。

NIO 引入了Channel、Buffer 和 Selector就是想把 IO 传输过程当中涉及到的信息具体化,让程序员有机会去控制它们。

当咱们进行传统的网络 IO 操做时,好比调用 write() 往 Socket 中的 SendQ 队列写数据时,当一次写的数据超过 SendQ 长度时,操做系统会按照 SendQ 的长度进行分割的,这个过程当中须要将用户空间数据和内核地址空间进行切换,而这个切换不是程序员能够控制的,由底层操做系统来帮咱们处理。

而在 Buffer 中,咱们能够控制 Buffer 的 capacity(容量),而且是否扩容以及如何扩容均可以控制。

理解了这些概念后咱们看一下,实际上它们是如何工做的呢?

仍是以上面的操做为例子,为了方便观看结果,本次的客户端线程请求数改为 15 个。

客户端,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

服务端,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

先启动服务端程序,再启动客户端程序,看看运行结果!

服务端,运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

客户端,运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

固然,客户端也不只仅只限制于 IO 的写法,还可使用SocketChannel来操做客户端,程序以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

同样的,先启动服务端,再启动客户端,客户端运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

从操做上能够看到,NIO 的操做比传统的 IO 操做要复杂的多!

Selector被称为选择器,固然你也能够翻译为多路复用器。它是 Java NIO 核心组件中的一个,用于检查一个或多个Channel(通道)的状态是否处于链接就绪接受就绪可读就绪可写就绪

如此能够实现单线程管理多个channels,也就是能够管理多个网络链接。

若是有人再问你 Java IO,就把这篇文章砸他头上

使用 Selector 的好处在于:相比传统方式使用多个线程来管理 IO,Selector 使用了更少的线程就能够处理通道了,而且实现网络高效传输!

虽然 java 中的 nio 传输比较快,为何你们都不肯意用 JDK 原生 NIO 进行开发呢?

从上面的代码中你们均可以看出来,除了编程复杂、编程模型难以外,还有几个让人诟病的问题:

  • JDK 的 NIO 底层由 epoll 实现,该实现饱受诟病的空轮询 bug 会致使 cpu 飙升 100%!
  • 项目庞大以后,自行实现的 NIO 很容易出现各种 bug,维护成本较高!

可是,Google 的 Netty 框架的出现,很大程度上改善了 JDK 原生 NIO 所存在的一些让人难以忍受的问题,关于 Netty 框架,会在后期的文章里进行介绍。

6.4.四、AIO

最后就是 AIO 了,全称 Asynchronous I/O,能够理解为异步 IO,也被称为 NIO 2,在 Java 7 中引入了 NIO 的改进版 NIO 2,它是异步非阻塞的 IO 模型,也就是咱们如今所说的 AIO。

异步 IO 是基于事件和回调机制实现的,也就是应用操做以后会直接返回,不会堵塞在那里,当后台处理完成,操做系统会通知相应的线程进行后续的操做。

客户端,程序示例:

若是有人再问你 Java IO,就把这篇文章砸他头上

服务端,程序示例:

若是有人再问你 Java IO,就把这篇文章砸他头上

一样的,先启动服务端程序,再启动客户端程序,看看运行结果!

服务端,运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

客户端端,运行结果以下:

若是有人再问你 Java IO,就把这篇文章砸他头上

这种组合方式用起来比较复杂,只有在一些很是复杂的分布式状况下使用,像集群之间的消息同步机制通常用这种 I/O 组合方式。如 Cassandra 的 Gossip 通讯机制就是采用异步非阻塞的方式。

Netty 以前也尝试使用过 AIO,不过又放弃了!

7、总结

本文阐述的内容较多,从 Java 基本 I/O 类库结构开始提及,主要介绍了 IO 的传输格式传输方式,以及磁盘 I/O 和网络 I/O 的基本工做方式。

本篇文章主要对 Java 的 IO 体系以及计算机部分网络基础知识作了些简单的介绍,其实每个模块涉及到的知识都很是很是多,在后期的文章中,会对各个模块进行详细的介绍,若是有理解不到的位置,欢迎指出!

以为文章不错就给小老弟点个关注吧,更多内容陆续奉上。

相关文章
相关标签/搜索