传统IO的种类
内核空间与用户空间
?
内核态与用户态的切换
//当前线程处于用户态
String str = "string";
int x = 2;
//切换至内核态
FileOutputStream fop = new FileOutputStream(new File("a.txt"));
OutputStreamWrite out = new OutputStreamWrite(fop, "GBK");
out.write("....");
out.append(‘\r\n‘);
out.close();
//用户态
int y = x + 2;
?
socket通信
- 服务器调用socket()、bind()、listen()完成初始化后,调用accept()阻塞等待,处于监听端口的状态,客户端调用socket()初始化后,调用connect()发出SYN段并阻塞等待服务器应答,服务器应答一个SYN-ACK段,客户端收到后从connect()返回,同时应答一个ACK段,服务器收到后从accept()返回。
- 数据传输的过程:
建立连接后,TCP协议提供全双工的通信服务,但是一般的客户端/服务器程序的流程是由客户端主动发起请求,服务器被动处理请求,一问一答的方式。因此,服务器从accept()返回后立刻调用read(),读socket就像读管道一样,如果没有数据到达就阻塞等待,这时客户端调用write()发送请求给服务器,服务器收到后从read()返回,对客户端的请求进行处理,在此期间客户端调用read()阻塞等待服务器的应答,服务器调用write()将处理结果发回给客户端,再次调用read()阻塞等待下一条请求,客户端收到后从read()返回,发送下一条请求,如此循环下去。- 如果客户端没有更多的请求了,就调用close()关闭连接,就像写端关闭的管道一样,服务器的read()返回0,这样服务器就知道客户端关闭了连接,也调用close()关闭连接。注意,任何一方调用close()后,连接的两个传输方向都关闭,不能再发送数据了。如果一方调用shutdown()则连接处于半关闭状态,仍可接收对方发来的数据。
public class EchoClient {
public static int DEFAULT_PORT = 9999;
public static void main(String[] args) throws IOException {
int port;
try {
port = Integer.parseInt(args[0]);
} catch(RuntimeException e) {
port = DEFAULT_PORT;
}
Socket socket = new Socket("127.0.0.1", port);
//键盘输入
BufferedReader buff = new BufferedReader(new InputStreamReader(System.in));
//Socket输出流,自动刷新
PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
//Socket输入流,读取服务端的数据并返回的大写数据
BufferedReader buffin = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String line = null;
while((line = buff.readLine()) != null) {
if("stop".equals(line)) {
break;
}
out.println(line);
// 读取服务端返回的一行大写数据
System.out.println(buffin.readLine());
}
}
}
也可使用linux下的nc命令代替客户端
public class EchoServer {
public static int DEFAULT_PORT = 9999;
public static void main(String[] args){
int port;
try {
port = Integer.parseInt(args[0]);
} catch(RuntimeException e){
port = DEFAULT_PORT;
}
try {
ServerSocket serverSocket = new ServerSocket(port);
Socket clientSocket = serverSocket.accept();
String ip = clientSocket.getInetAddress().getHostAddress();
System.out.println("port : " + port + ‘\t‘ + "ipaddress : " + ip);
//server 输出流对应client输入流,反之亦然
PrintWriter out = new PrintWriter(clientSocket.getOutputStream(), true);
BufferedReader in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
String inputline ;
while((inputline = in.readLine()) != null){
System.out.println(inputline);
out.println(inputline.toUpperCase());
}
} catch (IOException e) {
System.out.println("Exception caught when trying to listen on port" + port + "or listening for a connection");
e.printStackTrace();
}
}
}
同步与异步
描述的是用户线程与内核的交互方式或者说关注的是消息通信机制:
堵塞与非堵塞
关注的是用户线程调用内核 I/O 操作时,用户线程等待I/O操作完成前是否能做其他的事情:
阻塞与非阻塞主要是程序(线程)等待消息通知时的状态角度,同步与异步主要是从消息机制角度来说,这两组概念组合为四种情况,下面举几个网上的例子:
IO操作发生时会经历两个阶段:
下面简单介绍常见的五种 I/O 模型:
本节中将recvfrom函数视为系统调用。一般recvfrom函数的实现都有一个从应用程序进程中运行到内核中运行的切换,一段时间后再跟一个返回应用进程的切换。
阻塞 I/O
请求无法立即完成则保持阻塞。
进程阻塞的整段时间是指从调用recvfrom函数开始到它返回的这段时间,当进程返回成功提示时,应用进程开始处理数据报。
非阻塞 I/O
I/O 复用(异步堵塞I/O)
I/O 多路复用会用到 select 或者 poll 函数,这两个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这两个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时,才真正调用 I/O 操作函数。
从流程上看,使用select函数进行I/O请求和同步阻塞模型没有太大的区别,甚至还多了添加监视socket,以及调用select函数的额外操作,效率更差。但是,使用select函数的优势是用户可以在一个线程内同时处理多个socket的I/O请求。用户可以注册多个socket,然后不断地调用select来读取被激活的socket,达到在同一个线程内同时处理多个I/O请求的目的。而在同步阻塞模型中,必须通过多线程的方式才能达到这个目的。
I/O复用模型使用Reactor设计模式实现了这一机制。
调用select或poll函数的方法由一个用户态线程负责轮询多个socket,直到阶段1的数据就绪,再通知实际的用户线程执行阶段2的复制操作。通过一个专职的用户态线程执行非阻塞I/O轮询,模拟实现阶段1的异步化。
信号驱动I/O
首先,我们允许socket进行信号驱动I/O,并通过调用sigaction来安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好后,进程会收到一个SIGIO信号,可以在信号处理函数中调用recvfrom来读取数据报,并通知主循环数据已准备好被处理,也可以通知主循环,让它来读取数据报。
异步 I/O
调用 aio_read 函数,告诉内核描述字,缓冲区指针,缓冲区大小,文件偏移以及通知的方式,然后立即返回。当内核将数据拷贝到缓冲区后,再通知应用程序。
异步I/O模型使用Proactor设计模式实现了这一机制。
异步I/O模型告知内核:当整个过程(包括阶段1和阶段2)全部完成时,通知应用程序来读数据。
几种 I/O 模型的比较
前四种模型的区别是阶段1不相同,阶段2基本相同,都是将数据从内核拷贝到调用者的缓冲区。而异步 I/O 的两个阶段都不同于前四个模型。
同步 I/O 操作引起请求进程阻塞,直到 I/O 操作完成。异步 I/O 操作不引起请求进程阻塞。
关于这些模型的具体实现我打算放到Java I/O中进行讨论。
原文:https://www.cnblogs.com/skills/p/13321995.html