BIO简单使用

从本章开始,后面一段时间都学习网络和IO方面的知识。网络和IO都是知识盲区,慢慢整理,争取把这部分梳理清楚,首先IO是Input和Output的简写。代表输入和输出。而从位置来区分,又分为本地IO和网络IO。本地IO代表本机系统内的设备进行通信,网络代表本机系统和网络上的系统进行通信。而网络通讯模型又分为BIO(同步阻塞IO),NIO(同步非阻塞IO),AIO(异步非阻塞IO)。本章主要记录BIO的简单使用。

本地IO介绍

本地IO代表机器内部设备通信,比如,硬盘到内存,内存到网卡,内存到CPU等都是本地IO,我们以本地读写文件为例,其中包含了应用程序,CPU,内存,硬盘等设备。

读文件

Java代码指定文件位置,并通过流的方式读取文件。
我将Java读文件涉及的对象图像形式展示,如下:

  1. 应用程序通知操作系统需要访问某个位置的文件。
  2. 操作系统通知DMA进行操作
  3. DMA将硬盘上的资源加载到内核空间的缓冲区。
  4. 然后再将内核缓冲区的文件拷贝到用户空间的用户缓冲区。
  5. 实现对文件的访问功能

DMA 全称Direct Memory Access,即直接存储器访问。
DMA传输将数据从一个地址空间复制到另一个地址空间,提供在外设和存储器之间或者存储器和存储器之间的高速数据传输。当CPU初始化这个传输动作,传输动作本身是由DMA控制器来实现和完成的。DMA传输方式无需CPU直接控制传输,也没有中断处理方式那样保留现场和恢复现场过程,通过硬件为RAM和IO设备开辟一条直接传输数据的通道,使得CPU的效率大大提高。

写文件

  1. 执行读文件过程
  2. 修改用户空间内存缓冲区内容。
  3. 将用户缓冲区内容复制到内核缓冲区
  4. 调用sync刷新内存缓冲区的内容到硬盘

网络IO介绍

网络IO和本地IO不同的地方在于会使用到网卡,Internet,TCP协议等,下面将对TCP的发送和接收的网络模型进行整理.

网络发送


处理流程如下:

  1. 应用在用户空间将数据处理完成并写入应用缓冲区。
  2. 触发系统调用send命令将用户空间数据拷贝到内核空间的TCP发送缓冲区
  3. 最后通过DMA将内核缓冲区的数据复制到网卡缓冲区。

网络接收

网络接收正好相反。

  1. 首先网卡缓冲区接收到数据。
  2. 通过DMA将网卡缓冲区数据复制到TCP接收缓冲区
  3. 系统调用recv将TCP缓冲区数据拷贝到应用缓冲区

BIO

BIO介绍

上面我们提到了BIO(同步阻塞IO),那么什么是同步?什么是阻塞?
阻塞IO 和非阻塞IO 这两个概念是程序级别的。主要描述的是程序请求操作系统IO操作后,如果IO资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行(但是使用线程一直轮询,直到有IO资源准备好了)。

同步IO 和 异步IO,这两个概念是操作系统级别的。主要描述的是操作系统在收到程序请求IO操作后,如果IO资源没有准备好,该如何响应程序的问题:前者不响应,直到IO资源准备好以后;后者返回一个标记(好让程序和自己知道以后的数据往哪里通知),当IO资源准备好以后,再用事件机制返回给程序。
这个解答也是我在网络上看到的,暂时没有理清楚这个关系,等理解清楚再更新。

BIO编码实践

下面的代码实现了一个比较简单的长链接和短连接,没有心跳,连接池,只作为测试使用。

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
public class BioSocket {
private static final int PORT = 8080;
private static final String HOST = "127.0.0.1";
private static final ExecutorService executor = Executors.newCachedThreadPool();

public static void main(String[] args) {
executor.submit(BioSocket::start);
executor.submit((Runnable) () -> {
while (true){
BIOClient.sendMsg("msg: 1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
executor.submit((Runnable) () -> {
while (true){
BIOClient.sendMsg("msg: 2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
executor.submit((Runnable) () -> {
while (true){
BIOClient.sendMsg("msg: 3");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});


}

private static synchronized void start(){

try(ServerSocket serverSocket = new ServerSocket(PORT)) {
while (true){
System.out.println("服务端启动,可以处理连接了...");
Socket socket = serverSocket.accept();
// 启动新线程执行连接任务
executor.submit(new SocketHandler(socket));
}
} catch (IOException e) {
e.printStackTrace();
}
}

static class SocketHandler implements Runnable{
private Socket socket;

public SocketHandler(Socket socket) {
this.socket = socket;
}

@Override
public void run() {
try(BufferedReader r = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))) {
while (true){
String line = r.readLine();
//if (TextUtils.isEmpty(line)){
// System.out.println("执行结束,销毁任务...");
// return;
//}
if(!TextUtils.isEmpty(line)) {
System.out.println("服务器readline: " + line);
String result = "线程名称: " + Thread.currentThread().getName() + "消息内容: " + line;
w.write(result);
w.newLine();
w.flush();
}
}
} catch (IOException e) {
e.printStackTrace();
}

}
}

static class BIOClient{
public static void sendMsg(String msg){
try (Socket socket = new Socket(HOST,PORT);
BufferedReader r = new BufferedReader(new InputStreamReader(socket.getInputStream()));
BufferedWriter w = new BufferedWriter(new OutputStreamWriter(socket.getOutputStream()))){
w.write(msg);
w.newLine();
w.flush();
String readMsg = r.readLine();
if (!TextUtils.isEmpty(readMsg)){
System.out.println("客户端: "+readMsg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
}

说明:

  1. 使用缓存池复用线程,减少线程创建和CPU上下文切换问题。
  2. 通过读取数据为空检查是否释放连接(注释即可实现长连接)

总结

BIO模型及缺陷

我们运行上面的代码

代码中我们模拟长链接的实现,看到打印的日志中线程池的线程数不断的创建,大量的连接会创建大量的线程,但是连接是阻塞的,就算没有做任何事情,线程仍然不会被销毁。创建线程是需要占用内存资源的,维护线程是需要CPU资源的。每个系统可运行非常线程有限,所以不适合高并发的场景。

如果我们模拟短连接,并根据条件主动销毁无用连接,看看日志

使用短连接的方式保证了线程的复用,减小了CPU上下文切换,根据场景可以自适用线程数,这是一种伪异步IO的模式。但是底层还是使用同步阻塞模型。

在网上找了一份整理的Java IO流的表格整理。可以参考使用

作者

Labradors

发布于

2022-02-17

更新于

2022-02-18

许可协议

评论