2.2磁盘IO&网络IO工作机制
磁盘I/O工作机制:访问文件
在Java中,读 & 写对应了 read() & write() 两个系统调用,但只要系统调用,就会存在内核空间地址和用户空间地址切换的问题(操作系统为了保护系统安全,必须将内存空间和用户空间进行隔离),因为数据可能需要从内核空间向用户空间复制。
如果遇到了非常耗时的操作,如磁盘I/O,数据得从磁盘–>内核空间–>用户空间,复制了两遍,将会非常缓慢。
因此操作系统在内核空间加入了缓存机制,也就是说:如果用户程序访问的是同一段磁盘地址的数据,将会从上一次在内核空间的缓存中直接取得,这样就只复制了一次。
接下来会介绍几种访问文件的方式:
-
标准访问文件方式
-
当调用read()接口时,操作系统检查在内核的高速缓存中有没有数据,如果有就直接返回;否则从磁盘中读取,然后再缓存在内核中。
-
当调用write()接口时,数据从用户地址空间–>内核地址空间的缓存中,操作完成。
至于什么时候写到磁盘,由操作系统决定,除非调用sync同步,强制写入磁盘中。
-
-
直接I/O方式
- 程序直接访问磁盘数据,不经过操作系统内核,这样做能够减少一次数据复制。
- y优点:适用与由程序实现~~(而不是操作系统)~~的数据库管理系统等,因为操作系统不知道应该缓存哪些数据、失效哪些数据,但是程序知道。
- 缺点:如果访问的数据不在缓存中,那么每次都会直接从磁盘加载,而这种加载相对非常慢,多次之后会变得十分低效。
-
同步访问文件方式
- 数据的读&写都是同步操作,和标准访问不同的是:只有当数据被成功写到磁盘时,才返回标志给应用程序
- 性能差,只针对于数据安全性要求高的场景,通常由硬件完成
-
异步访问文件方式
- 当访问数据的线程发出请求后,线程会接着处理其他事情,而不是阻塞(等待),只有当数据返回时才继续处理后续操作。
- 能提高程序效率,但不是访问文件的效率
-
内存映射方式
- 将操作系统内存中的某块区域和磁盘中的文件夹关联,访问内存时转化为访问磁盘文件的某段数据(和直接IO的区别就是这个映射,相当于一个定位,就快的多)
- 目的是减少数据的复制操作,此时这两个空间的数据都相当于是共享的
Java序列化&反序列化
Java序列化就是将一个对象转化为一串二进制的字节数组,然后通过保存这个字节数组进行持久化。而且达到持久化的目的,必须实现java.io.Serializable接口。
Java反序列化就是将字节数组再重新构造成对象。
序例化对象
要序例化一个对象,分如下步骤:
- 创建某种OutputStream对象,然后将其封装在一个ObjectOutputStream对象内
- 调用writeObject()即可将对象序列化(对象序列化基于字节,因此使用InputStream和OutputStream继承类层次结构)。
反序列化和序列化过程正好相反,需要将一个InputStream封装在ObjectInputStream内,然后调用readObject()获取一个引用,它指向一个向上转型的Object,所以必须向下转型才能直接设置它们。
public class Serialize implements Serializable {
public int num = 1;
public long id = 777;
public static void main(String[] args) throws IOException {
//初始化
FileOutputStream fos = new FileOutputStream("filepath");
ObjectOutputStream oos = new ObjectOutputStream(fos);
//开始干事情
Serialize sl = new Serialize();//我自己写的对象
oos.writeObject(Serialize);
oos.flush();
oos.close();
}
}
另外,在纯Java环境下,Java序列化能够很好地工作;但是在多语言环境下,会出问题,尽量还是用JSON或XML这些通用的数据结构。
网络I/O工作机制
基础知识
首先你得懂TCP的三次握手和四次挥手,在《计算机网络》中有详细说明,不做过多解释。
要想加速网络的I/O,首先要分析一下影响网络的因素:
- 网络带宽:1s内能传输的最大比特数,平均网络带宽为1.7Mb/s
- 传输距离:也就是数据要在光纤中走的距离,因为有一个折射率,所以大概只有光速的2/3,比如杭州和青岛的两台机子进行同步操作必定会有一个30ms的延时
- TCP拥塞控制:详细见《计算机网络》
Java Socket工作机制
Socket 是描述计算机之间完成相互通信一种抽象功能。下面将通过一组比喻&一张图让你了解Socket的工作机制:
- 两台主机=两个城市
- 物理链路=高速通道
- Socket=交通工具
- 要交付的数据=要运输的货物
- 通信协议=交通工具的相关规则(
比如说电动车不准上高速[狗头])
大致过程:
但是一台主机上可能运行着多个应用程序,所以,就要通过 TCP 或 UDP 的地址也就是端口号来指定,端口号又对应一个Socket。这样就可以通过一个 Socket 实例,唯一代表一条通信链路了。下面将具体介绍是如何建立链路的。
建立通信链路
当客户端要与服务端通信:
在客户端
- 客户端首先要创建一个 Socket 实例,操作系统将为这个 Socket 分配一个端口号(port)
- 创建一个套接字数据结构,包含本地和远程地址&端口号。(这个数据结构将一直保存在系统中直到这个连接关闭)
- 进行 TCP 的三次握手协议
- Socket 实例的构造函数正确返回,Socket 实例对象就宣布正式创建完成,否则将抛出 IOException 错误。
在服务端
-
服务端将创建一个 ServerSocket 实例(比较简单,只要端口号未占用,一般实例创建都会成功)
-
操作系统也会为 ServerSocket 实例创建一个底层数据结构,包含指定监听的端口号和包含监听地址的通配符(通常情况下都是“*”即监听所有地址)
-
调用 accept() 方法时,进入阻塞状态,等待客户端的请求。
-
当一个新的请求到来时,将为这个连接创建一个新的套接字数据结构(包含请求源地址和端口)。
-
这个数据结构,将会关联到 ServerSocket 实例的一个未完成的连接“数据结构“列表中(注意这时服务端与之对应的 Socket 实例并没有正式创建完成,待三次握手完成后,就会将这个 Socket 实例对应的数据结构从未完成列表中移到已完成列表中)
所以 ServerSocket 所关联的列表中每个数据结构,都代表与一个客户端的建立的 TCP 连接(无论是否完成)
数据传输
传输数据是我们建立连接的主要目的,如何通过 Socket 传输数据,下面将详细介绍。
-
当连接已经建立成功,服务端和客户端都会拥有一个 Socket 实例,两边的 Socket 实例都有一个 InputStream 和 OutputStream,正是通过这两个对象来交换数据。
-
当 Socket 对象正式创建完毕时,操作系统将会为 InputStream 和 OutputStream 分别分配一定大小的缓冲区,以便数据的写入和读取
-
写入端将数据写到 OutputStream 对应的 SendQ 队列中,当队列填满时,数据就将会一起发送,到读取端的InputStream 的 RecvQ 队列中。(如果这时 RecvQ 已经满了,那么 OutputStream 的 write 方法将会阻塞直到 RecvQ 队列有足够的空间容纳 SendQ 发送的数据)
注意:
- 这个缓存区的大小&写入端的速度&读取端的速度,非常影响该连接的数据传输效率,所以在写入和读取还要有一个协调的过程
- 如果两边同时传送数据时可能会产生死锁,在后面 NIO 部分将介绍避免这种情况。