请先了解一下:同步、异步、阻塞、非阻塞
阻塞式I/O
应用进程发起一个 I/O请求会经历两种状态
- 等待数据进入内核空间
- 从内核空间拷贝数据到用户空间
在这两个阶段 用户进程都处于阻塞状态,首先,在等待数据就绪这段时间,用户空间什么都不干,就等待数据就绪,当数据就绪,拷贝数据阶段用户进程依然处于阻塞状态,这就是阻塞式 I/O 模型
// 服务端代码
//创建套接字,绑定并监听指定端口
ServerSocket serverSocket = new ServerSocket(20000);
//请求未到达之时,线程阻塞于此
client = serverSocket.accept();
// 客户端连接成功,输出提示
System.out.println("客户端连接成功");
// 启动一个新的线程处理客户端请求
new Thread(new ServerThread(client)).start();
// 子线程中处理客户端的输入
class ServerThread implements Runnable {
.....
@Override
public void run() {
boolean flag = true;
while (flag) {
// 读取客户端发送来的数据
String str = buf.readLine();
// 回复给客户端 get 表示收到数据
out.println("get");
}
}
}
//客户端代码
Socket client = new Socket("127.0.0.1", 20000);
boolean flag = true;
while (flag) {
// 读取用户从键盘的输入
String str = input.readLine();
// 把用户的输入发送给服务端
out.println(str);
// 接受到服务端回传的 get 字符串
String echo = buf.readLine();
System.out.println(echo);
}
}
从代码可知,每当有客户端请求建立连接时,服务端就会开启一个线程,这模式对于服务端来说压力很大。
当调用readline 的时候会阻塞,当readline()没有读取到数据的时候,会一直阻塞,服务端为了能同时执行多个线程,就不得不创建大量的线程。
非阻塞式 I/O
阻塞式I/O是对非阻塞式I/O的改进,当等待数据进入内核空间时用户进程可以不用阻塞,而是给用户进程返回一个0,用户线程可以去干一些其他的事情,在干其他的事的过程中,用户进程会不断的发送同样的请求,直到数据到达为止。
拷贝数据:拷贝数据到用户空间依然会阻塞。
I/O 多路复用
非阻塞式I/O 虽然没有阻塞数据,但是它一直不停的轮循去看数据是否准备好,依然会花费掉cpu大量的时间。
既然客户端不会因为数据还未准备好而阻塞,那我们就没有必要为每个客户端都分配一个线程去轮循的判断数据是否可读,可以选择一个线程去监听所有的客户端,然后由这个线程轮循去判断数据是否准备好,如果数据可读,就通知其它线程。这种方式就是I/O的复用。
信号驱动式I/O
信号驱动式IO就是指进程预先告知内核,当某个描述符上发送事件时,内核使用信号通知相关进程。信号驱动式IO并没有实现真正的异步,因为通知到进程之后,依然是由进程来完成IO操作。
异步 I/O
前面的四种形式总的来说都是同步的I/O,在数据的拷贝过程中,用户进程总是阻塞的。
异步 I/O 就是用户发起请求,无论数据是否准备好都返回给用户,用户可以去干自己的事了,当数据准备好,内核直接把数据拷贝给用户,然后给用户通知一下就OK。这种模式下,等待数据和拷贝数据都是不用阻塞的。