Java 的 I/O 操作类在包 java.io 下,大概有将近 80 个类,但是这些类大概可以分成四组,分别是:
基于字节操作的 I/O 接口:InputStream 和 OutputStream
基于字符操作的 I/O 接口:Writer 和 Reader
基于磁盘操作的 I/O 接口:File
基于网络操作的 I/O 接口:Socket(Socket 类并不在 java.io 包下)
基于字节的 I/O 操作接口
基于字节的 I/O 操作接口输入和输出分别是:InputStream 和 OutputStream
流最终写到什么地方必须要指定,要么是写到磁盘要么是写到网络中,写网络实际上也是写文件,只不过写网络还有一步需要处理就是底层操作系统再将数据传送到其它地方而不是本地磁盘。
输入字节流: InputStream 所有输入字节流的基类
--FileInputStream 读取文件的输入字节流
--BufferedInputStream 缓冲输入字节流。提高读取文件数据的效率。内部维护一个8KB的字节数组而已。
输出字节流: OutputStream 所有输出字节流的基类
--FileInputStream 向文件输出数据的 输出字节流
--BufferedInputStream 缓冲输出字节流。提高向文件写数据的效率。内部维护一个8KB的字节数组而已。
什么情况下使用字节流:
读取数据不需要经过编码或解码的情况下。 (不需要我们看得懂)例如:图片数据
基于字符的 I/O 操作接口
字符流 = 字节流 + 编码(解码)
不管是磁盘还是网络传输,最小的存储单元都是字节,而不是字符,所以 I/O 操作的都是字节而不是字符。但是为啥有操作字符的 I/O 接口呢?这是因为我们的程序中通常操作的数据都是以字符形式,为了操作方便当然要提供一个直接写字符的 I/O 接口,如此而已。字符到字节必须要经过编码转换,而这个编码又非常耗时,而且还会经常出现乱码问题,所以 I/O 的编码问题经常是让人头疼的问题。
输入字符流: Reader -> 所有输入字符流的基类 --FileReader -> 读取文件字符的输入字符流 --BufferReader -> 缓冲输入字符流。提高读取文件字符的效率且多了readLine的功能。内部维护一个8192个长度的字符数组。
输出字符流: Writer -> 所有输出字符流的基类 --FileWriter -> 向文件输出字符数据的输出字符流 --BufferReader -> 缓冲输出字符流。提高写文件的效率且多了newLine()。
什么情况下使用字符流:如果读写的数据都是字符流。
字节与字符的转化接口
数据持久化或网络传输都是以字节进行的,所以必须要有字符到字节或字节到字符的转化。字符到字节需要转化,其中读的转化过程如下图所示:
清单 1.读取文件
try {
StringBuffer str = new StringBuffer();
char[] buf = new char[1024];
FileReader f = new FileReader("file");
while(f.read(buf)>0){
str.append(buf);
}
str.toString();
} catch (IOException e) {}
FileReader 类就是按照上面的工作方式读取文件的,FileReader 是继承了 InputStreamReader 类,实际上是读取文件流,然后通过 StreamDecoder 解码成 char,只不过这里的解码字符集是默认字符集。
写入也是类似的过程如下图所示:
通过 OutputStreamWriter 类完成,字符到字节的编码过程,由 StreamEncoder 完成编码过程。
转换流: 输入字节流的转换流:InputStreamReader(输入字节流转换为输入字符流)
输出字节流的转换流:OutputStreamWriter(输出字节流转换为输出字符流)
作用:
1。可以把对用的字节流转换成字符流 2。可以指定码表进行读写文件的数据。(例如FileReader, FileWriter这两个类默认GBK编码表,不能由你指定码表读写文件数据)
例子:
public static void readFile() throws IOException{
FileInputStream fileInputStream = new FileInputStream("F:\\a.txt");\
//创建输入字节流的转换流
InputStreamReader inputStreamReader = new InputStreamReader(fileInputStream);
int content = 0;
while((content=inputStreamReader.read())!=-1) {
System.out.println((char)content);
}
//关闭资源
inputStreamReader.close();
}
磁盘 I/O 工作机制
数据在磁盘的唯一最小描述就是文件,也就是说上层应用程序只能通过文件来操作磁盘上的数据,文件也是操作系统和磁盘驱动器交互的一个最小单元。
下面为从磁盘读取文件的示意图:
当传入一个文件路径,将会根据这个路径创建一个 File 对象来标识这个文件,然后将会根据这个 File 对象创建真正读取文件的操作对象,这时将会真正创建一个关联真实存在的磁盘文件的文件描述符 FileDescriptor,通过这个对象可以直接控制这个磁盘文件。
由于我们需要读取的是字符格式,所以需要 StreamDecoder 类将 byte 解码为 char 格式,至于如何从磁盘驱动器上读取一段数据,由操作系统帮我们完成。
Java Socket 的工作机制
Socket 这个概念没有对应到一个具体的实体,它是描述计算机之间完成相互通信一种抽象功能。
下图是典型的基于 Socket 的通信的场景:
主机 A 的应用程序要能和主机 B 的应用程序通信,必须通过 Socket 建立连接,而建立 Socket 连接必须需要底层 TCP/IP 协议来建立 TCP 连接。
建立 TCP 连接需要底层 IP 协议来寻址网络中的主机。
网络层使用的 IP 协议可以帮助我们根据 IP 地址来找到目标主机,但是一台主机上可能运行着多个应用程序,如何才能与指定的应用程序通信就要通过 TCP 或 UPD 的地址也就是端口号来指定。这样就可以通过一个 Socket 实例唯一代表一个主机上的一个应用程序的通信链路了。
建立通信链路
客户端首先要创建一个 Socket 实例,操作系统将为这个 Socket 实例分配一个没有被使用的本地端口号,并创建一个包含本地和远程地址和端口号的套接字数据结构,这个数据结构将一直保存在系统中直到这个连接关闭。在创建 Socket 实例的构造函数正确返回之前,将要进行 TCP 的三次握手协议,TCP 握手协议完成后,Socket 实例对象将创建完成,否则将抛出 IOException 错误。
与之对应的服务端将创建一个 ServerSocket 实例,ServerSocket 创建比较简单只要指定的端口号没有被占用,一般实例创建都会成功,同时操作系统也会为 ServerSocket 实例创建一个底层数据结构,这个数据结构中包含指定监听的端口号和包含监听地址的通配符,通常情况下都是“*”即监听所有地址。
当调用 accept() 方法时,将进入阻塞状态,等待客户端的请求。当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构,该套接字数据的信息包含的地址和端口信息正是请求源地址和端口。这个新创建的数据结构将会关联到 ServerSocket 实例的一个未完成的连接数据结构列表中,注意这时服务端与之对应的 Socket 实例并没有完成创建,而要等到与客户端的三次握手完成后,这个服务端的 Socket 实例才会返回,并将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中。所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接。
数据传输
当连接已经建立成功,服务端和客户端都会拥有一个 Socket 实例,每个 Socket 实例都有一个 InputStream 和 OutputStream,正是通过这两个对象来交换数据。
网络 I/O 都是以字节流传输的。当 Socket 对象创建时,操作系统将会为 InputStream 和 OutputStream 分别分配一定大小的缓冲区,数据的写入和读取都是通过这个缓存区完成的。
写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据将被发送到另一端 InputStream 的 RecvQ 队列中,如果这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据。
参考资料:
- https://www.ibm.com/developerworks/cn/java/j-lo-javaio 深入分析 Java I/O 的工作机制
网友评论