1. I/O模型

1.1 I/O模型的基本说明

  1. I/O模型简单的理解:就是用什么样的数据通道进行数据的发送和接受,很大程度上决定了IO程序的性能

  2. java支持三种IO模型:BIO, NIO, AIO

  3. java BIO: 同步非阻塞模型(传统阻塞型),服务器实现模式为一个连接一个线程,即客户端有一个连接,服务器就新建一个线程进行处理,如果这个连接不释放,这个线程就会阻塞在那里造成不必要的线程开销。

    在这里插入图片描述

  4. java NIO:同步非阻塞,服务器实现模式为一个线程处理多个请求(连接),即客户端发送的请求连接会注册到多路复用器上,多路复用器轮询到连接有IO请求就会进行处理。

    在这里插入图片描述

  5. java AIO: 异步非阻塞,AIO引入了异步通道的概念,采用了Proactor模式,简化了程序编写,有效的请求才启动线程,他的特点是操作系统完成后才通知服务端程序启动线程去处理,一般使用于连接数量较多,连接时间较长的应用。

2. BIO,NIO,AIO使用场景分析

  1. BIO适用于 连接数量少,架构固定的应用,对服务器资源要求高。程序简单容易理解.JDK 1.4以前的选择。
  2. NIO适用于连接数量多,连接时间短(轻操作)的架构,比如聊天服务器,弹幕系统,服务器间通讯等,编程较为复杂。JDK1.4之后开始支持。
  3. AIO适用于连接数量多,连接时间长(重操作)的架构,比如相册服务器,充分调用OS参与并发操作,编程比较复杂。JDK1.7之后开始支持

3. Java BIO基本介绍

  1. java BIO 是传统的IO编程,相关的类和接口都在java.io
  2. BIO(Blocing IO):同步阻塞模型,服务器实现模式为一个连接对应一个线程进行处理,如果连接不做任何事情会造成不必要的线程开销。
  3. BIO适用于连接数量较少且固定的架构,对服务器资源要求较高

4. Java BIO的工作机制

在这里插入图片描述

  1. 服务端会启动一个serversocket
  2. 客户端启动socket对服务器进行通信,默认情况下服务端对客户端的每一个连接都要新建一个线程处理该连接
  3. 客户端发出请求后,看服务器是否有现成线程相应,没有会等待,或者拒绝。
  4. 如果有响应,客户端线程会等待请求结束后,再继续执行。

5. Java BIO应用举例

5.1 需求

  1. 使用BIO模型编写一个服务器,监听6666端口,当有客户端连接时,启动一个线程与其通信。
  2. 要求使用线程池,可以连接多个客户端。
  3. 服务端接受用户发送的数据(终端中用Telnet方式发送数据)
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
import java.io.InputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class BIOServer {
public static void main(String[] args) throws Exception {

//线程池机制

//思路
//1. 创建一个线程池
//2. 如果有客户端连接,就创建一个线程,与之通讯(单独写一个方法)

ExecutorService newCachedThreadPool = Executors.newCachedThreadPool();

//创建ServerSocket
ServerSocket serverSocket = new ServerSocket(6666);

System.out.println("服务器启动了");

while (true) {

System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
//监听,等待客户端连接
System.out.println("等待连接....");
final Socket socket = serverSocket.accept(); // 会阻塞,主线程一直阻塞在这里等待新的连接
System.out.println("连接到一个客户端");

//就创建一个线程,与之通讯(单独写一个方法)
newCachedThreadPool.execute(new Runnable() {
public void run() { //我们重写
//可以和客户端通讯
handler(socket);
}
});

}


}

//编写一个handler方法,和客户端通讯
public static void handler(Socket socket) {

try {
System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());
byte[] bytes = new byte[1024];
//通过socket 获取输入流
InputStream inputStream = socket.getInputStream();

//循环的读取客户端发送的数据
while (true) {

System.out.println("线程信息 id =" + Thread.currentThread().getId() + " 名字=" + Thread.currentThread().getName());

System.out.println("read....");
int read = inputStream.read(bytes); // 会阻塞。子线程会阻塞在这里接受新的消息。
if(read != -1) {
System.out.println(new String(bytes, 0, read
)); //输出客户端发送的数据
} else {
break;
}
}


}catch (Exception e) {
e.printStackTrace();
}finally {
System.out.println("关闭和client的连接");
try {
socket.close();
}catch (Exception e) {
e.printStackTrace();
}

}
}
}

5.2 分析

  1. Main线程监听6666端口,一旦有客户端请求连接,从线程池中new 一个线程thread_i进行处理。
  2. thread_i线程处理socket连接,对对应客户端进行数据Read,业务处理,数据write等操作。
  3. 并发数较大的时候,创建大量线程,系统资源占用较大。
  4. 连接建立后,如果当前线程没有数据可读,thread_i会阻塞到Read操作上,造成线程阻塞,系统资源浪费。