# 重要概念

  • 阻塞IO非阻塞IO

这两个概念是 程序级别 的。主要描述的是程序请求操作系统 IO 操作后,如果 IO 资源没有准备好,那么程序该如何处理的问题:前者等待;后者继续执行 (并且使用线程一直轮询,直到有 IO 资源准备好了)。

程序级别的差异表现在开发人员如何编写代码来实现不同功能。

  • 同步IO非同步IO

这两个概念是 操作系统级别 的。主要描述的是操作系统在收到程序请求 IO 操作后,如果 IO 资源没有准备好,该如何响应程序的问题:前者不响应,直到 IO 资源准备好以后;后者返回一个标记 (好让程序和自己知道以后的数据往哪里通知),当 IO 资源准备好以后,再用事件机制返回给程序。

# 传统的 BIO 通信简介

我们这里讲的 IO 是以套接字为数据载体的 BS 架构层面,以前大多数网络通信是阻塞模式:

  • 客户端向服务器端发出请求后,客户端会一直等待 (不会再做其他事情),直到服务器端返回结果或者网络出现问题。

  • 服务器端同样的,当在处理某个客户端 A 发来的请求时,另一个客户端 B 发来的请求会等待,直到服务器端的这个处理线程完成上一个处理。

BS示意图

尽管可以使用多线程,主线程接收请求,其他线程来处理请求。但是仍然存在局限,数据报文的接收仍然需要一个一个的来,并且在 Linux 系统中,可以创建的线程是有限的。每个线程都是较大的资源消耗, JVM 创建一个线程时,回味起分配堆栈空间,默认为 128K 。如果应用程序大量使用长连接,线程不会关闭,系统资源消耗容易失控(此处可以联想 QQ,如果不发消息,该线程也会被占用)。

# 代码实践

我们通过 socket 模拟 BIO 的实现逻辑

建立 Server ,建立 ServerSocket 对象,绑定端口,等待连接,如果连接成功就新建一个线程去处理连接

public class server {
    private static Socket socket=null;
    public static void main(String[] args) {
        try {
            // 绑定端口
            ServerSocket serverSocket = new ServerSocket();
            serverSocket.bind(new InetSocketAddress(8080));
            
            // 死循环一直等待客户端发送请求
            while (true){
                // 等待连接  阻塞
                System.out.println("等待连接");
                socket = serverSocket.accept();
                System.out.println("连接成功");
                // 连接成功后新开一个线程去处理这个连接
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        byte[] bytes = new byte[1024];
                        try {
                            System.out.println("等待读取数据");
                            // 等待读取数据    阻塞
                            int length = socket.getInputStream().read(bytes);
                            System.out.println(new String(bytes,0,length));
                            System.out.println("数据读取成功");
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    }
                }).start();
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

建立 Client -- 客户端代码

public class Client {
    public static void main(String[] args) {
        Socket socket= null;
        try {
            socket = new Socket("127.0.0.1",8080);
            socket.getOutputStream().write("一条数据".getBytes());
            socket.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

从代码可以看到,当服务端接收到一个客户端请求时,会新开一个线程来执行任务,在 IO 时会被阻塞。从前面我们对各种 IO 讲解时知道, read 方法,如果 IO 没有准备好,就会阻塞。不论是阻塞 IO,非阻塞 IO,多路复用,信号驱动,都会在 read 方法存在阻塞的情况。

如果操作系统没有发现有套接字从指定的端口 X 来,那么操作系统就会等待。这样 serverSocket.accept() 方法就会一直等待。这就是为什么 accept() 方法为什么会阻塞:它内部的实现是使用的操作系统级别的同步 IO。

# 参考

掘金:https://juejin.cn/post/6974191624331100174

Java 全栈知识体系:https://pdai.tech/md/java/io/java-io-bio.html