原来传统BIO的局限性在这里!

大家都知道传统的 BIO 网络模型有各种各样的缺点,于是就有了关于 NIO 网络模型的出现,更多的人也都开始喜欢使用 Netty 这种框架来进行开发,而摒弃了传统的 BIO 的模型,今天阿粉就给大家说一下为什么这么多人对 BIO 网络模型这么的头疼。

BIO

BIO 网络模型实际上就是使用传统的 Java IO 编程,相关联的类和接口都在 java.io 下。

BIO 模型到底是个什么玩意?

BIO (blocking I/O) 同步阻塞,我们看这个翻译,blocking I/O,实际上就能看出来,就是阻塞,当我们的客户端发起请求的时候,服务端就会开启一个线程,专门为这个客户端提供对应的读写操作,只要客户端发起了,这个服务端的线程就一直保持存在,就算客户端啥也不干,那也在那里开着,就是玩。

不过说一句,现在用 BIO 的不是很多了,因为强制使用的时代过去了,现在还有用 JDK1.4 以下的项目么?估计应该是没有了,阿粉之前曾经维护过一个使用 JRUN 的项目,使用的JDK的版本,就是非常古老的。

我们来整一个服务端和客户端来看看是什么样子的模型。

Server端:

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
public class TimeServer {
    public static void main(String[] args){
        ServerSocket server=null;
        try {
            server=new ServerSocket(18080);
            System.out.println("服务启动 端口:18080...");
            while (true){
                Socket client = server.accept();
                //每次接收到一个新的客户端连接,启动一个新的线程来处理
                new Thread(new TimeServerHandler(client)).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }finally {
            try {
                server.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

public class TimeServerHandler implements Runnable{
    private Socket clientProxxy;

    public TimeServerHandler(Socket clientProxxy) {
        this.clientProxxy = clientProxxy;
    }

    @Override
    public void run() {
        BufferedReader reader = null;
        PrintWriter writer = null;
        try {
            reader = new BufferedReader(new InputStreamReader(clientProxxy.getInputStream()));
            writer =new PrintWriter(clientProxxy.getOutputStream()) ;
            while (true) {//因为一个client可以发送多次请求,这里的每一次循环,相当于接收处理一次请求
                String request = reader.readLine();
                if (!"GET CURRENT TIME".equals(request)) {
                    writer.println("BAD_REQUEST");
                } else {
                    writer.println(Calendar.getInstance().getTime().toLocaleString());
                }
                writer.flush();
            }
        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            try {
                writer.close();
                reader.close();
                clientProxxy.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }
}

Client端:

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
public class TimeClient {
    public static void main(String[] args)  {
        BufferedReader reader = null;
        PrintWriter writer = null;
        Socket client=null;
        try {
            client=new Socket("127.0.0.1",18080);
            writer = new PrintWriter(client.getOutputStream());
            reader = new BufferedReader(new InputStreamReader(client.getInputStream()));

            while (true){//每隔5秒发送一次请求
                writer.println("GET CURRENT TIME");
                writer.flush();
                String response = reader.readLine();
                System.out.println("Current Time:"+response);
                Thread.sleep(5000);
            }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            try {
                writer.close();
                reader.close();
                client.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

    }
}

执行结果如下:

1
2
3
4
5
6
7
8
9
10
11
12

服务端:
服务启动 端口:18080...

客户端:
Current Time:2021-6-16 11:37:34
Current Time:2021-6-16 11:37:39
Current Time:2021-6-16 11:37:44
Current Time:2021-6-16 11:37:49
Current Time:2021-6-16 11:37:54
Current Time:2021-6-16 11:37:59
Current Time:2021-6-16 11:38:04

我们使用的是 Client 发送请求指令”GET CURRENT TIME”给server端,每隔5秒钟发送一次,每次 Server 端都返回当前时间。

这就相当于是多个 Client 同时请求 Server ,每个 Client 创建一个线程来进行处理.

Accpetor thread 只负责与 Client 建立连接,worker thread用于处理每个thread真正要执行的操作。

在到了这里的时候,我们就发现了一些事情,感觉不对了有没有,

BIO 的局限性一

  • 每一个 Client 建立连接后都需要创建独立的线程与 Server 进行数据的读写,业务处理。

这时候就会出现什么样子的问题,我们如果说有上千个客户端的时候,我们服务端就会创建上千个线程,有多少客户端,就创建多少个线程,这对于 Java 来说, 代价实在是太大。

如果说我们线程在到达一定数量的时候,我们在做线程的切换的时候,大家可以想象一下对资源的浪费是什么样子的,一个线程和一百个线程甚至超过一千个线程的时候,在线程进行上下文切换的时候,会出现什么样子的问题。

针对某个线程来说,这种 BIO 的模型,在这个线程读取数据的时候,如果没有数据了,那线程就开始了阻塞,为了能够做到响应数据,这个线程在一直阻塞,这时候有新的请求的时候,线程阻塞,好吧,那我只能等,一直处于一个等待不能执行的状态。

BIO 的局限性二

  • 并发数大的时候,会创建非常多的线程来处理连接,系统资源会出现非常大的开销。

BIO 的局限性三

  • 在线程阻塞的时候,会造成资源的浪费。

实际上说是三个局限性,总得来说,他就是一个局限,浪费资源,开销大,这是非常致命的,一个小小的功能的话,你占用的服务器大量的资源,只是硬件上这块的内容,都得增加多少的成本,现在万恶的资本家们,抱着能省就省的原则,又扯远了,我们还是回归到 BIO 上。

而且这种 BIO 的模型,在本质上说,实际上就相当于是一个 1:1 的关系,而这种 1:1 的关系如果在客户端非常多的时候,创建的线程数所浪费的资源是非常巨大的,所以就出现了另外一种模型,NIO 模型。而 NIO 模型实际上目前使用的那可真的是太普遍了,比如说 Netty ,都是选择使用这种模型,不再继续使用 BIO 的模型了。

而关于 NIO 阿粉已经说了很多次,文章都写了好多,阿粉把文章都给大家放在下面,按照顺序阅读,不然阿粉怕大家会有点混乱

什么是BIO,NIO?他们和多路复用器有啥关系?

Java:前程似锦的 NIO 2.0

用Socket编程?我还是选择了Netty

而据说 JDK1.8 之后的 原生NIO API 中,会有空轮询的bug,会让服务器的 CPU 瞬间飙升到100%,具体真假,阿粉反正是没有碰到过,也没有测试出来,真的假的就留给大家去试试了。

文章参考

《田守枝的Java技术博客》 《Netty权威指南》

Java Geek Tech wechat
欢迎订阅 Java 极客技术,这里分享关于 Java 的一切。