BIO、NIO、AIO等IO模式详解(图文、代码示例解说)

1.BIO(blocking I/O)

BIO是一个传统的IO,是一个阻塞的IO,当然都是这么说,那么堵塞在哪里呢,我们通过代码示例给大家解说

我先用程序模拟一个客户端连接服务端程序,建立一个socket连接来监听客户端,然后监听到了以后用getInputStream进行获取流然后死循环监听客户端的请求数据。

/**
 * 用BIO方式让客户端连接程序,监听服务端
 *
 * @Author df
 * @Date 2020/4/12 15:24
 * @Version 1.0
 */
public class BioServer {
    public static void main(String[] args) {
         try (ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("BIOServer has started,Listening on port" + serverSocket.getLocalSocketAddress());
            while (true) {
                Socket clientSocket = serverSocket.accept();
                System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
                // BIO阻塞原因:getInputStream阻塞一直占用当前线程资源,不让当前线程做其他事情
                Scanner input = new Scanner(clientSocket.getInputStream());
                //针对每个socket,不断的进行数据交互
                while (true) {
                    String request = input.nextLine();
                    if ("quit".equals(request)) {
                        break;
                    }
                    System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
                    String response = "From BIOServer Hello " + request + ".\n";
                    clientSocket.getOutputStream().write(response.getBytes());
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

咱们来启动一下main方法,启动完成,监听本地的8888端口

接下来就用cmd命令telnet来连接服务,输入如下命令回车,如下第二张图就连接上了

输入字符敲回车,服务端都能收到了

然后打开第二个cmd窗口,同样输入telnet localhost 8888然后回车,你发现控制台没有再打印出第二个连接信息了,输入任何字符也都没有输出了,那么为什么呢?

当第一个线程请求以后,socket建立连接打印信息,一直循环等待,等第二个线程需要进来的时候就不能进行访问了,因为一直被第一个线程getInputStream堵塞着。也就是说阻塞一直占用当前线程资源,不让当前线程做其他事情。

那么这样阻塞也不是办法啊,那我难道只能进行一次连接么,这样肯定是不行的,那需要解决这个问题啊!

既然BIO通过获取getInputStream进行阻塞,那么可以不可以用多线程的方式解决这个问题呢,让客户端能够多个连接呢。也给大家准备了线程池改良版的代码示例

/**
 * 线程池版改良BIO阻塞问题
 *
 * @Author df
 * @Date 2020/4/12 16:09
 * @Version 1.0
 */
public class BioServerThreadPool {

    Map<Socket, String> map;

    public static void main(String[] args) {
        // 创建3个线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);
        RequestHandler requestHeader = new RequestHandler();
        try (ServerSocket serverSocket = new ServerSocket(8888)) {
            System.out.println("BIO thread Server has started,Listening on port" + serverSocket.getLocalSocketAddress());
            while (true) {
                Socket clientSocket = serverSocket.accept();
                // 线程提交
                executor.submit(new ClientHandler(clientSocket, requestHeader));
                System.out.println("Connection from " + clientSocket.getRemoteSocketAddress());
            }
        } catch (
                IOException e) {
            e.printStackTrace();
        }

    }
/**
 * @Author df
 * @Date 2020/4/12 20:52
 * @Version 1.0
 */
public class ClientHandler implements Runnable {
    private static RequestHandler requestHandler;
    private static Socket clientSocket;

    public ClientHandler(Socket clientSocket, RequestHandler requestHandler) {
        this.requestHandler = requestHandler;
        this.clientSocket = clientSocket;
    }

    @Override
    public void run() {
        try (Scanner input = new Scanner(clientSocket.getInputStream())) {
            while (true) {
                String request = input.nextLine();
                if ("quit".equals(request)) {
                    break;
                }
                System.out.println(String.format("From %s : %s", clientSocket.getRemoteSocketAddress(), request));
                String response = "From BIOServer Hello " + request + ".\n";
                clientSocket.getOutputStream().write(response.getBytes());
            }
        } catch (Exception e) {

        }
    }
}
public class RequestHandler {
    public String handle(String request) {
        return "From Server Hello " + request + ".\n";
    }
}

咱们来启动一下,打开cmd同样输入telnet localhost 8888然后回车,发现可以连接,并且也正常通信的

那么再打开一个cmd,输入telnet localhost 8888然后回车,看是否能连接呢?发现第二个也能连接并且正常通信

那么打开第三个呢,能否连接呢?发现也是能连接上的,并且正常通信

那么第四个呢?发现可以连接,但是无法进行任何输出操作,为什么?

这时候就把请求操作都放到等待队列里了,所以你能有连接操作,但是已经被其他三个线程阻塞掉了,不能再有线程处理它了,那么这个时候咱们在其他三个已经连接成功的输入quit回车将其连接退出,又会发生什么呢?你会发现第四个线程立马把之前阻塞时候进行输入的字符全部输出,并且可以进行正常的通信了

其实tomcat中就是用这种BIO的方式进行请求连接,防止阻塞用线程池方式,默认线程池设置200个。所以多线程是解决阻塞的一种方式。

但是啊,在高并发场景下我有10000个人请求,如果选用线程池,就算开1000个线程池,9999个放入了等待队列里,而且等待队列也不是源源不断的存储的,再说了让用户等待这种设计是不行的,而且多线程切换的开销也大,所以这个是jdk的BIO留下的坑,那么jdk会解决,所以JDK1.4以后出现了NIO

那么再说NIO之前一定要知道为什么IO是阻塞的呢?所以就说一下IO的阻塞流程!

1.1 BIO阻塞的过程!

其实IO是否阻塞和java代码没有直接关系,IO的本质(针对java而言):应用程序和操作系统内核进行数据交互。

为什么这么说呢?假如我们要读取硬盘的gupao.txt文件一定是要inputStream读取,但是我们的java程序一定能直接读取磁盘么,答案是否的也是没有权限的,需要操作系统帮助我们读取,我们java程序是进程也可以叫做用户空间,用户空间是程序员可以操作的,可以直接对硬盘操作的就是内核空间。下图在进行讲解一下

以读操作为例说一下过程:

  1. java程序发出读操作,内核空间收到请求通过磁盘控制器转化对磁盘进行操作。
  2. 操作以后返回数据通过DMA方式将数据放入内核空间的缓冲区里面。
  3. Java read是从缓冲区拿数据进行返回的。

所以走到这里应该明白阻塞不阻塞的与我们程序没有关系,我们也不会无缘无故的阻塞,阻塞不阻塞和内核空间才有关系。

但是JDK设计者想好了该如何解决,因为以前的BIO是我发起了read请求以后一直等待内核空间读取放入缓冲然后用户空间才读取,内核空间没有操作完它就会一直等待,但是NIO就是我发起了read请求以后我就不管了我还可以做其他的事情,直到你把数据读取完返回给我,我再操作,这样就是非阻塞的了

把以上的叙述讲解完大家就应该理解了,接下来说NIO

2.NIO(non-blocking I/O)

那么这张图就是同步非阻塞也就是NIO,首先application就是用户空间,kernel就是内核空间。

1.用户通过read,在这里就是recvfrom(java也要调用c语言去读取的)去读取,内核空间接收到了进行操作,然后用户空间不需要等待直接可以去干别的事情,然后再来询问内核空间处理好了么,没有的话用户空间还可以干别的,直到在继续询问内核空间发现内核空间告诉它自己处理好了(内核空间把数据拷贝到缓冲区)那么进行返回,结束

那么NIO方式的代码示例我也准备好了,给大家演示一下

public class NIOServer {

    public static void main(String[] args) {
        try {
            ServerSocketChannel serverChannel = ServerSocketChannel.open();
            // 设置不阻塞
            serverChannel.configureBlocking(false);
            serverChannel.bind(new InetSocketAddress(9999));
            System.out.println("NIO NIOServer has started ,listening on port:" + serverChannel.getLocalAddress());

            Selector selector = Selector.open();

            // 每个客户端来了之后,就把客户端注册到selector选择器上,默认状态就是ACCEPT
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            ByteBuffer buffer = ByteBuffer.allocate(1024);
            RequestHandler requestHandler = new RequestHandler();
            // 轮询,服务端不断轮询,等待客户端的连接
            while (true) {
                int select = selector.select();
                if (select == 0) {
                    continue;
                }
                // 如果selector有的话,那么就取出对应的channel
                Set<SelectionKey> selectionKeys = selector.selectedKeys();
                Iterator<SelectionKey> iterator = selectionKeys.iterator();
                while (iterator.hasNext()) {
                    SelectionKey key = iterator.next();
                    // 判断SelectionKey中的channel状态如何
                    if (key.isAcceptable()) {
                        ServerSocketChannel channel = (ServerSocketChannel) key.channel();
                        SocketChannel clientChannel = channel.accept();
                        // 客户端的channel来源打出来
                        System.out.println("Connection from " + clientChannel.getRemoteAddress());
                        // 将客户端的也设置为非阻塞
                        clientChannel.configureBlocking(false);
                        // 将channel的状态设置为read
                        clientChannel.register(selector, SelectionKey.OP_READ);
                    }
                    // 接下来轮询到的时候发现状态是readable
                    if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel) key.channel();
                        // 数据的交互是以buffer为中间桥梁的
                        channel.read(buffer);
                        // 用buffer取数据也用buffer返回数据
                        String request = new String(buffer.array()).trim();
                        buffer.clear();
                        System.out.println(String.format("From %s : %s", channel.getRemoteAddress(), request));
                        String response = requestHandler.handle(request);
                        channel.write(ByteBuffer.wrap(response.getBytes()));
                    }
                    iterator.remove();
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

你们自行用之前的方法测试把,打开5,7八个都不会阻塞

3.AIO(NIO.2) (Asynchronous I/O) 

异步非阻塞,服务器实现模式为一个有效请求一个线程,客户端I/O请求都是由OS先完成了再通知服务器应用启动线程进行处理。

1.当应用空间要读取的时候发送给内核空间,内核空间不管有没有处理直接返回,然后等到内核空间处理完毕直接发送信号(deliver signal)把数据传送给用户空间。

4.多路复用IO

1.每一次的连接读取也好写操作也好都先不进行连接,都先存储也可以说是登记到这边,不会直接分配线程去调度资源,直到真的要操作input或Out的时候我才去分配线程做这些操作呢。

1.有操作进行连接都会通过select进行注册,轮询监控发现有需要读取或者写操作再去分配线程,那么之后的操作可以使用同步非阻塞或者异步非阻塞或者阻塞IO都是可以的。

以上就是全部内容,以上笔记记录学习来源咕泡教育-Jack老师的公开课学习整理!

 

猜你喜欢

转载自blog.csdn.net/dfBeautifulLive/article/details/105770926