一、简介
说到 I/O,想必大家都不会生疏,I/O 英语全称:Input/Output,即 输出 / 输入 ,通常 指数据在外部存储器和内部存储器或其余周边设备之间的输出和输入。
比方咱们罕用的 SD 卡 、 U 盘、 移动硬盘 等等存储文件的硬件设施,当咱们将其插入电脑的 usb 硬件接口时,咱们就能够从电脑中读取设施中的信息或者写入信息,这个过程就波及到 I/O 的操作。
当然,波及 I/O 的操作,不仅仅局限于硬件设施的读写,还要网络数据的传输,比方,咱们在电脑上用浏览器搜寻互联网上的信息,这个过程也波及到 I/O 的操作。
无论是从磁盘中读写文件,还是在网络中传输数据,能够说 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 只是人与机器或者机器与机器交互的伎俩,除了在它们可能实现这个交互性能外,咱们关注的就是如何进步它的运行效率了,而 数据格式 和传输方式 是影响效率最要害的因素。
本文前面,也是基于这两个点进行深刻开展剖析。
二、基于字节操作的接口
基于字节的输出和输入操作接口别离是:InputStream 和 OutputStream。
2.1、字节输出流
InputStream 输出流的类继承档次如下图所示:
输出流依据数据节点类型和解决形式,别离能够划分出了若干个子类,如下图:
OutputStream 输入流的类层次结构也是相似。
2.2、字节输入流
OutputStream 输入流的类继承档次如下图所示:
输入流依据数据节点类型和解决形式,也别离能够划分出了若干个子类,如下图:
在这里就不具体的介绍各个子类的应用办法,有趣味的敌人能够查看 JDK 的 API 阐明文档,笔者也会在前期的文章会进行具体的介绍,这里只是重点想说一下,无论是输出还是输入,操作数据的形式能够组合应用,各个解决流的类并不是只操作固定的节点流,比方如下输入形式:
// 将文件输入流包装到序列化输入流中,再将序列化输入流包装到缓冲中
OutputStream out = new BufferedOutputStream(new ObjectOutputStream(new FileOutputStream(new File("fileName")));
另外,输入流最终写到什么中央必须要指定,要么是写到硬盘中,要么是写到网络中,从图中能够发现,写网络实际上也是写文件,只不过写到网络中,须要通过底层操作系统将数据发送到其余的计算机中,而不是写入到本地硬盘中。
三、基于字符操作的接口
不论是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符,然而为什么要有操作字符的 I/O 接口呢?
这是因为咱们的程序中通常操作的数据都是以字符模式,为了程序操作更不便而提供一个间接写字符的 I/O 接口,仅此而已。
基于字符的输出和输入操作接口别离是:Reader 和 Writer,下图是字符的 I/O 操作接口波及到的类结构图。
3.1、字符输出流
Reader 输出流的类继承档次如下图所示:
同样的,输出流依据数据节点类型和解决形式,别离能够划分出了若干个子类,如下图:
3.2、字符输入流
Writer 输入流的类继承档次如下图所示:
同样的,输入流依据数据节点类型和解决形式分类,别离能够划分出了若干个子类,如下图:
不论是 Reader 还是 Writer 类,它们都只定义了读取或写入数据字符的形式,也就是说要么是读要么是写,然而并没有规定数据要写到哪去,写到哪去就是咱们前面要探讨的基于磁盘或网络的工作机制。
四、字节与字符的转化
刚刚咱们说到,不论是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,设计字符的起因是为了程序操作更不便,那么怎么将字符转化成字节或者将字节转化成字符呢?
InputStreamReader 和 OutputStreamWriter 就是转化桥梁。
4.1、输出流转化过程
输出流字符解码相干类构造的转化过程如下图所示:
从图上能够看到,InputStreamReader 类是字节到字符的转化桥梁,其中 StreamDecoder
指的是一个 解码 操作类,Charset
指的是字符集。
InputStream 到 Reader 的过程须要指定编码字符集,否则将采纳操作系统默认字符集,很可能会呈现乱码问题,StreamDecoder 则是实现字节到字符的解码的实现类。
关上源码局部,InputStream 到 Reader 转化过程
public class InputStreamReader extends Reader {
private final StreamDecoder sd;
/**
* Creates an InputStreamReader that uses the default charset.
*
* @param in An InputStream
*/
public InputStreamReader(InputStream in) {super(in);
try {sd = StreamDecoder.forInputStreamReader(in, this, (String)null); // ## check lock object
} catch (UnsupportedEncodingException e) {
// The default encoding should always be available
throw new Error(e);
}
}
4.2、输入流转化过程
输入流转化过程也是相似,如下图所示:
通过 OutputStreamWriter 类实现字符到字节的编码过程,由 StreamEncoder
实现 编码 过程。
源码局部,Writer 到 OutputStream 转化过程:
public class OutputStreamWriter extends Writer {
private final StreamEncoder se;
public OutputStreamWriter(OutputStream out) {super(out);
try {se = StreamEncoder.forOutputStreamWriter(out, this, (String)null);
} catch (UnsupportedEncodingException e) {throw new Error(e);
}
}
五、基于磁盘操作的接口
后面介绍了 Java I/O 的操作接口,这些接口次要定义了如何操作数据,以及介绍了操作数据格式的形式:字节流和字符流。
还有一个关键问题就是数据写到何处,其中一个次要的解决形式就是将数据长久化到物理磁盘。
咱们晓得数据在磁盘的惟一最小形容就是文件,也就是说下层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的一个最小单元。
在 Java I/O 体系中,File 类是惟一代表磁盘文件自身的对象。
File 类定义了一些与平台无关的办法来操作文件,包含查看一个 文件是否存在、创立、删除文件、重命名文件、判断文件的读写权限是否存在、设置和查问文件的最近批改工夫 等等操作。
值得注意的是 Java 中通常的 File 并不代表一个实在存在的文件对象,当你通过指定一个门路描述符时,它就会返回一个代表这个门路相关联的一个 虚构对象,这个可能是一个实在存在的文件或者是一个蕴含多个文件的目录。
例如,读取一个文件内容,程序如下:
public static void main(String[] args) throws IOException {StringBuffer sb = new StringBuffer();
char[] chars = new char[1024];
FileReader f = new FileReader("fileName");
while (f.read()>0){sb.append(chars);
}
sb.toString();}
以下面的程序为例,从硬盘中读取一段文本字符,操作流程如下图:
咱们再来看看源码执行流程。
当咱们传入一个指定的文件名来创立 File 对象,通过 FileReader 来读取文件内容时,会主动创立一个 FileInputStream
对象来读取文件内容,也就是咱们上文中所说的字节流来读取文件。
public class FileReader extends InputStreamReader {
/**
* Creates a new <tt>FileReader</tt>, given the name of the
* file to read from.
*
* @param fileName the name of the file to read from
* @exception FileNotFoundException if the named file does not exist,
* is a directory rather than a regular file,
* or for some other reason cannot be opened for
* reading.
*/
public FileReader(String fileName) throws FileNotFoundException {super(new FileInputStream(fileName));
}
紧接着,会创立一个 FileDescriptor
的对象,其实这个对象就是真正代表一个存在的文件对象的形容。能够通过 FileInputStream
对象调用 getFD()
办法获取真正与底层操作系统关联的文件形容。
public
class FileInputStream extends InputStream
{
/* 文件形容 */
private final FileDescriptor fd;
/* 文件门路 */
private final String path;
public FileInputStream(File file) throws FileNotFoundException {String name = (file != null ? file.getPath() : null);
SecurityManager security = System.getSecurityManager();
if (security != null) {security.checkRead(name);
}
if (name == null) {throw new NullPointerException();
}
if (file.isInvalid()) {throw new FileNotFoundException("Invalid file path");
}
fd = new FileDescriptor();
fd.attach(this);
path = name;
open(name);
}
因为咱们须要读取的是字符格局,所以须要 StreamDecoder
类将 byte
解码为 char
格局,至于如何从磁盘驱动器上读取一段数据,由操作系统帮咱们实现。
六、基于网络操作的接口
持续来说说数据写到何处的另一种解决形式:将数据写入互联网中以供其余电脑能拜访。
6.1、Socket 简介
在事实中,Socket 这个概念没有一个具体的实体,它是形容计算机之间实现互相通信一种形象定义。
打个比方,能够把 Socket 比作为两个城市之间的交通工具,有了它,就能够在城市之间来回穿梭了。并且,交通工具有多种,每种交通工具也有相应的交通规则。Socket 也一样,也有多种。大部分状况下咱们应用的都是基于 TCP/IP 的流套接字,它是一种稳固的通信协议。
典型的基于 Socket 通信的应用程序场景,如下图:
主机 A 的应用程序要想和主机 B 的应用程序通信,必须通过 Socket 建设连贯,而建设 Socket 连贯必须须要底层 TCP/IP 协定来建设 TCP 连贯。
6.2、建设通信链路
咱们晓得网络层应用的 IP 协定能够帮忙咱们依据 IP 地址来找到指标主机,然而一台主机上可能运行着多个应用程序,如何能力与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就能够通过一个 Socket 实例代表惟一一个主机上的一个应用程序的通信链路了。
为了准确无误地把数据送达指标处,TCP 协定采纳了三次握手策略,如下图:
其中,SYN 全称为 Synchronize Sequence Numbers,示意同步序列编号,是 TCP/IP 建设连贯时应用的握手信号。
ACK 全称为 Acknowledge character,即确认字符,示意发来的数据已确认接管无误。
在客户机和服务器之间建设失常的 TCP 网络连接时,客户机首先收回一个 SYN 音讯,服务器应用 SYN + ACK 应答示意接管到了这个音讯,最初客户机再以 ACK 音讯响应。
这样在客户机和服务器之间能力建设起牢靠的 TCP 连贯,数据才能够在客户机和服务器之间传递。
简略流程如下:
- 发送端 –(发送带有 SYN 标记的数据包)–> 承受端(第一次握手);
- 承受端 –(发送带有 SYN + ACK 标记的数据包)–> 发送端(第二次握手);
- 发送端 –(发送带有 ACK 标记的数据包)–> 承受端(第三次握手);
实现三次握手之后,客户端应用程序与服务器应用程序就能够开始传送数据了。
传输数据是咱们建设连贯的次要目标,如何通过 Socket 传输数据呢?
6.3、传输数据
当客户端要与服务端通信时,客户端首先要创立一个 Socket 实例,默认操作系统将为这个 Socket 实例调配一个没有被应用的本地端口号,并创立一个蕴含本地、近程地址和端口号的套接字数据结构,这个数据结构将始终保留在零碎中直到这个连贯敞开。
/**
* 客户端
*/
public class Client {public static void main(String[] args) throws IOException {Socket socket = new Socket("127.0.0.1", 9090);
// 向服务端发送数据
PrintStream ps = new PrintStream(new BufferedOutputStream(socket.getOutputStream()));
// 读取服务端返回的数据
BufferedReader br = new BufferedReader(new InputStreamReader(socket.getInputStream()));
ps.println("hello word!!");
ps.flush();
String info = br.readLine();
System.out.println(info);
ps.close();
br.close();}
}
与之对应的服务端,也将创立一个 ServerSocket 实例,ServerSocket 创立比较简单,只有指定的端口号没有被占用,个别实例创立都会胜利,同时操作系统也会为 ServerSocket 实例创立一个底层数据结构,这个数据结构中蕴含指定监听的端口号和蕴含监听地址的通配符,通常状况下都是 *
即监听所有地址。
之后当调用 accept() 办法时,将进入阻塞状态,期待客户端的申请。
/**
* 服务端
*/
public class ServerTest {public static void main(String[] args) throws IOException {
// 初始化服务端端口 9090
ServerSocket serverSocket = new ServerSocket(9090);
System.out.println("服务端已启动,端口号为 9090...");
// 开启循环监听
while (true) {
// 期待客户端的连贯
Socket accept = serverSocket.accept();
// 将字节流转化为字符流,读取客户端发来的数据
BufferedReader br = new BufferedReader(new InputStreamReader(accept.getInputStream()));
// 一行一行的读取客户端的数据
String s = br.readLine();
System.out.println("服务端收到客户端的信息:" + s);
}
}
}
咱们先启动服务端程序,再运行客户端,服务端收到客户端发送的信息,服务端打印后果如下:
留神,客户端只有与服务端建设三次握手胜利之后,才会发送数据,而 TCP/IP 握手过程,底层操作系统曾经帮咱们实现了!
当连贯曾经建设胜利,服务端和客户端都会领有一个 Socket 实例,每个 Socket 实例都有一个 InputStream 和 OutputStream,正如咱们后面所说的,网络 I/O 都是以字节流传输的,Socket 正是通过这两个对象来替换数据。
当 Socket 对象创立时,操作系统将会为 InputStream 和 OutputStream 别离调配肯定大小的缓冲区,数据的写入和读取都是通过这个缓存区实现的。
写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据将被发送到另一端 InputStream 的 RecvQ 队列中,如果这时 RecvQ 曾经满了,那么 OutputStream 的 write 办法将会阻塞直到 RecvQ 队列有足够的空间包容 SendQ 发送的数据。
值得特地留神的是,缓存区的大小以及写入端的速度和读取端的速度十分影响这个连贯的数据传输效率,因为可能会产生阻塞,所以网络 I/O 与磁盘 I/O 在数据的写入和读取还要有一个协调的过程,如果两边同时传送数据时可能会产生死锁的问题。
如何进步网络 IO 传输效率、保障数据传输的牢靠,曾经成了工程师们急需解决的问题。
6.4、IO 工作形式
在计算机中,IO 传输数据有三种工作形式,别离是 BIO、NIO、AIO。
下期咱们再一个个剖析这三种 IO 的特点及原理。
七、总结
本文论述的内容较多,从 Java 根本 I/O 类库构造开始说起,次要介绍了 IO 的 传输格局 和传输方式,以及磁盘 I/O 和网络 I/O 的根本工作形式。