IO读写原理
用户进程进行的IO读写,都会用到read和write两大系统调用。
read系统调用:将数据从内核缓冲区复制到进程缓冲区中。
write系统调用:将数据从进程缓冲区复制到内核缓冲区。
在操作系统需要访问系统资源时,需要借助于内核态,系统资源主要有:CPU、输入输出、进程管理、内存、进程间通信。这些系统资源,在用户线程中是无法被直接访问的,只有通过操作系统来访问。所以把操作系统访问这些资源的功能,称为系统调用。
缓冲区:目的是为了减少频繁的系统IO调用。
系统调用需要从用户态切换到内核态,切换之后保存用户进程的数据和状态等信息。
结束调用之后需要恢复之前的信息,为了消除这种损耗的时间,还有损耗性能的时间,就有了缓冲区。有了缓冲区,操作系统使用read函数从内核缓冲区赋值到进程缓冲区,write函数从进程缓冲区复制到内核缓冲区,只有缓冲区中的数据达到一定量时,再进行IO系统调用。
阻塞与非阻塞:阻塞和非阻塞用来描述用户线程调用内核IO操作的方式。
阻塞是指IO操作需要彻底完成后,才返回到用户空间。
非阻塞指IO操作被调用后,立即给用户一个状态,无需等到IO操作彻底完成。
Java I/O底层流程
四大模型之:BIO同步阻塞IO
客户端每发一次请求,服务端都会创建一个新的线程来处理这个请求。即使这个请求什么都不干,服务器依然会创建一个新的线程来处理。
同步阻塞IO,客户端使用Sokcet,服务端使用:ServerSocket。
客户端使用Socket指定一个IP地址和端口号,此时操作系统会为Socket实例分配一个未使用的端口号,并创建一个包含本地和远程地址、端口号的数据结构,这个数据结构会一直保存,直到连接结束。在真正完成Socket实例创建之前,会进行3次握手,握手成功后,才算真正的创建了Socket实例。
服务端使用ServerSocket指定一个端口号,操作系统也会为ServerSocket实例创建一个包含端口号和监听的地址的通配符的数据结构,之后,会调用accept()
,用来监听这个端口上的连接请求。这个方法是阻塞的,只有一个新连接请求来时,才会继续往下执行。新连接请求来时,会创建一个新的Socket数据结构,这个数据结构里包含着源地址和要请求的端口号信息。这个新的Socket会关联到ServerSocket一个未完成的连接数据结构中,此时ServerSocket还不算创建成功,直至三次握手后,才算创建成功。ServerSocket所关联的列表中每一个数据结构,都代表一个TCP连接。
BIO示意图:
即,在用户发起IO系统调用时,用户线程会进入阻塞状态。在等待内核缓冲区的数据复制到用户缓冲区这个过程是阻塞的,直到IO调用完毕,用户线程才会不阻塞。
BIO的一对一实现(一个服务器对应一个客户端请求):
//BIOServer:
public class BIOServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
BufferedReader bufferedReader = null;
BufferedOutputStream bos = null;
try {
serverSocket = new ServerSocket(3333);
System.out.println("服务端已启动...");
// 监听3333端口上的连接请求,该方法是一个阻塞方法,
// 如果没有请求,则会一直阻塞,直到有请求。
Socket accept = serverSocket.accept();
//执行到这,说明有请求
System.out.println("连接建立成功");
while(true){
//服务器先读取客户端的消息:
// 调用accept.getInputStream,获取连接的字节流。
// 再将字节流转换为字符流,在添加一层缓冲流。加快读取
bufferedReader = new BufferedReader(new InputStreamReader(accept.getInputStream()));
// 获取流中的信息
String s = bufferedReader.readLine();
if ("exit".equals(s)){
break;
}