knight_ka | 生活及学习笔记

BIO、NIO、AIO编程详解(上)

BIO、NIO、AIO编程详解(上)

@(IO)[BIO|NIO|AIO]

在进行I/O编程之前,首先要理解I/O的四个特点:阻塞(Blocking-IO),非阻塞(non-blocking),同步(synchronous),异步(asynchronous )

把他们阻塞在一起就有了:

  • 同步阻塞IO(Java BIO)
  • 同步非阻塞IO(Java NIO)
  • 异步非阻塞IO(Java AIO)

概念解释

  • 什么是同步(synchronous)?什么是异步(asynchronous)?

    同步(synchronous): 你总是做完一件再去做另一件,不管是否需要时间等待,这就是同步(就是在发出一个功能调用时,在没有得到结果之前,该调用就不返回,即此时不能做下一件事情);

    异步(asynchronous):异步则反之,你可以同时做几件事,并非一定需要一件事做 完再做另一件事(当一个异步过程调用发出后,调用者不能立刻得到结果,此时可以接着做其它事情)。同步简单理解成一问一答同步进行,异步可以简单理解为不必等一个问题有了答案再去问另一个问题,尽管问,有答了再通知你。

  • 什么是阻塞(Blocking)?什么是非阻塞(non-Blocking)?

    阻塞和非阻塞是针对于当前线程的状态而言的。如果程序调用一个函数,这个函数还没返回结果时,当前线程被挂起了,那么这个程序就是阻塞的。如果程序调用一个函数时,这个函数还没返回结果时,当前线程还在处理其它的事情,那么这个程序是非阻塞程序。

    比如说:你去饭店吃饭,当你点了饭,饭还没做好的时候,如果你是阻塞式调用,你就会把自己“挂起”,知道你点的饭做好之后;如果你是非阻塞式调用,不管饭有没有做好,你自己去做别的事去了,只不过过几分钟来检查一下饭有没有做好。

可以看到阻塞与非阻塞的区别是自己本身的状态,阻塞是自己被挂起了,不能做任何事情了;非阻塞是自己还可以去做别的事情。而同步非同步指的是两件事之间的顺序,如果一件事做完才能做另一件事,那就是同步;如果两件事可以同时进行,那就是异步。

  • 阻塞和同步的区别!

    阻塞并不代表同步!
    如同步非阻塞型函数(Socket的recv函数),在socket中调用recv函数,如果缓冲区中没有数据,这个函数就会一直等待,直到有数据才返回。而此时,当前线程还会继续处理各种各样的消息。这时候是同步但不是阻塞。也就是我必须在饭做好之后才可以吃饭,但是我在饭还没做好的时候可以去做其它事情。

    如异步阻塞型I/O,两件事可以同时进行,但是这两件事都是阻塞型事情。如吃饭和踢球这两件事可以同时进行,但是在吃饭的时候,自己处于“挂起”状态,不能去做其它事情了。这时候是异步阻塞。

有时候换一种思考方式就好理解了,如同步就是饭店做饭和自己吃饭的关系;异步就是饭店做饭和自己踢球的关系;而阻塞是指自己在做其中某一件事情时的状态,如在饭店做饭的时候,自己如果只是在原地默默的等着,那就是阻塞;如果饭店在做饭的时候,自己还可以去做别的事情,就是非阻塞。阻塞与非阻塞在代码中只是设计上的不同。

阻塞与非阻塞的着重点是:自己的状态。
同步与非同步的着重点是:两件事情的顺序。

java传统BIO模型的实践与分析

分析

本文对IO模型的实践全都是以网络socket通信为原型进行分析与实践。
bio模型
基于BIO的Socket通讯过程为:

    1. 服务端正常启动,打开端口,并阻塞在accept()方法。此时线程被挂起,系统不会分配CPU时间片给此线程。
    1. 有客户端打开连接。此时服务端停止阻塞,线程处于就绪状态,随时可以获取到CPU的执行权。
    1. 服务端开启一个新线程用来处理本次请求。
    1. 客户端与服务端进行I/O交互。(同步且阻塞式交互)

重点是看第4、5两步。

客户端和服务端进行交互的顺序一般为:1.客户端发送消息到服务端 2.服务端接受消息并处理完毕后,写入响应消息返回给客户端 3.客户端读取响应回来的消息 4.读取后进行反馈,再次发送消息到服务端 …

当一个客户端连接到服务端,服务端还处理完毕的时候,其它的客户端无法连接到服务端(这就是它的同步特点!可以通过多线程实现异步,但是并不是BIO本身提供的机制)。

用传统的Socket方式进行通讯可以发现,客户端和服务端使用这种I/O方式通讯只能是一问一答模式的。也就是说客户端发出消息,只有当服务端响应消息之后客户端才能再次发送消息(这里就是I/O的同步特点),虽然可以通过线程池开启新线程的方式进行异步处理,解除了一问一答的限制,但是无法从根源上解决问题,因为读取和写入都是阻塞式方法,下面查看Java API文档:

查看java的InputStream的read()方法上的API说明:

This method blocks until input data is available, end of file is detected, or an exception is thrown.

read方法是阻塞方法,当读取输入流的时候它会一直阻塞下去,除非发生以下三种情况:

    1. 有数据可读
    1. 可用数据已经读取完毕
    1. 发生空指针或者I/O异常

查看OutputStream的write()方法上的API说明:

Writes an array of bytes.This method will block until the bytes are actually written.

write方法写输出流的时候,它将会被阻塞,直到所有要发送的字节全部写入完毕,或者发生异常。

通过对输入输出流的API文档分析,我们可以发现写入流和读取流中的数据都是阻塞的,阻塞的时间取决于对方I/O线程的处理速度和网络I/O的传输速度。

也就是虽然可以通过线程池的方式达到伪异步处理,但是这种I/O方式在进行读流或者写流的时候都可能出现阻塞的情况,在实际生产中,我们的应用依赖于服务端的处理速度,所以它的可靠性就比较差。采用多线程方式进行伪异步处理,如果服务端处理60s,那么我们的I/O线程就会阻塞60s,并且开启线程会加大服务器开销。

下面是伪异步I/O模式的代码展示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
package com.designPattern.Reactor.BIO2;
import java.io.*;
import java.net.Socket;
/**
* BIO 客户端
* Created by Tnp.
*/
public class BIOClient {
public static void main(String[] args) {
BufferedWriter bufferedWriter = null;
BufferedReader bufferedReader = null;
try{
//1.创建连接服务器的socket
Socket socket = new Socket("127.0.0.1",9999);
//2.获取输出流
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
//3.获取输入流
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
while(true){ //循环读取与发送
//4.发送内容
bufferedWriter.write("Query tIme");
bufferedWriter.newLine();
bufferedWriter.flush();
//5.读取内容
/***
* 如果要解除一问一答的限制,这里需要开启一个新线程进行读取服务端响应回来的消息。(略)
*/
String line = bufferedReader.readLine();
System.out.println(line);
}
}catch (Exception e){
e.printStackTrace();
}finally{
try{
if(bufferedReader != null){
bufferedReader.close();
bufferedReader = null;
}
if(bufferedWriter != null){
bufferedWriter.close();
bufferedWriter = null;
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}
package com.designPattern.Reactor.BIO2;
import java.io.IOException;
import java.net.ServerSocket;
import java.net.Socket;
/**
* BIO服务端
* Created by Tnp.
*/
public class BIOServer {
public static void main(String[] args) {
ServerSocket serverSocket = null;
try{
//1.开启本地端口
serverSocket = new ServerSocket(9999);
while(true){
//2.阻塞线程-等待连接
Socket socket = serverSocket.accept();
//3.开启线程处理该请求
new Thread(new BIOServerHelper(socket)).start();
}
}catch (IOException e){
e.printStackTrace();
}
}
}
package com.designPattern.Reactor.BIO2;
import java.io.*;
import java.net.Socket;
import java.util.Date;
/**
* Created by Tnp.
*/
public class BIOServerHelper implements Runnable {
private Socket socket;
public BIOServerHelper(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
BufferedReader bufferedReader = null;
BufferedWriter bufferedWriter = null;
try{
//1.获取输入流
bufferedReader = new BufferedReader(new InputStreamReader(socket.getInputStream()));
//2.获取输出流
bufferedWriter = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()));
while(true){
//3.获取请求内容
String req;
req = bufferedReader.readLine();
System.out.println(req);
/**
* 如果要解除一问一答的限制,这里需要开启一个新线程进行写入流来响应客户端的消息。(略)
*/
//4.响应内容
bufferedWriter.write(new Date(System.currentTimeMillis()).toString());
bufferedWriter.newLine();
bufferedWriter.flush();
}
}catch(IOException e){
e.printStackTrace();
}finally{
try{
if(bufferedReader != null){
bufferedReader.close();
bufferedReader = null;
}
if(bufferedWriter != null){
bufferedWriter.close();
bufferedWriter = null;
}
}catch(IOException e){
e.printStackTrace();
}
}
}
}

总结

JAVA BIO是同步阻塞型IO,同步体现在当一个用户连接到服务端,并且没有服务端处理完这个用户请求的时候,下一个用户就无法连接到服务端(这一点可以通过线程池优化,但是并解决不了BIO的本质,BIO还是同步的IO.);阻塞体现在:1.它是面向流的IO.读流或者写流操作都是阻塞的,如果没有数据可读或者数据还没有写入完成,那么线程就会阻塞在这里;2.调用serverSocket.accept()方法时也是阻塞的。所以说BIO是同步阻塞型IO.

server导致阻塞的原因:

1、serversocket的accept方法,阻塞等待client连接,直到client连接成功。

2、线程从socket inputstream读入数据,会进入阻塞状态,直到全部数据读完。

3、线程向socket outputstream写入数据,会阻塞直到全部数据写完。

client导致阻塞的原因:

1、client建立连接时会阻塞,直到连接成功。

2、线程从socket输入流读入数据,如果没有足够数据读完会进入阻塞状态,直到有数据或者读到输入流末尾。

3、线程从socket输出流写入数据,直到输出所有数据。

4、socket.setsolinger()设置socket的延迟时间,当socket关闭时,会进入阻塞状态,直到全部数据都发送完或者超时。
既然NIO是非阻塞型IO,那么NIO要解决的问题就是1.在客户端与服务端进行交互期间不会因为处理速度等原因而使线程处于阻塞;2.服务端开启之后不会因为没有用户连接而阻塞。

未完
下一篇:BIO、NIO、AIO编程详解(中)