BIO/NIO的区别是什么?

 BIO(Blocking I/O)同步阻塞I/O​

  • 核心机制​

        同步阻塞:线程在读写数据时会被阻塞,直到操作完成。

        线程模型:每个连接由一个独立的线程处理

        实现方式:基于InputStream/OutputStream,典型如ServerSocketSocket。以下是基于BIO模式实现的服务端。

// 服务端代码(BIO)
ServerSocket server = new ServerSocket(8080);
while (true) {
    Socket client = server.accept(); // 阻塞等待连接
    new Thread(() -> {
        // 每个连接分配一个线程处理读写
        InputStream in = client.getInputStream();
        // 读取数据(阻塞)
        in.read(...);
    }).start();
}

        在上面的服务端代码中,server.accept()是阻塞方法,也就是当没有连接时,主线程会被阻塞住。当收到连接后,阻塞方法执行,主线程往下执行代码,为请求分配一个新的线程去处理。子线程去处理这个连接的读写操作。而主线程会回到循环的开始,在server.accept()继续阻塞,等待下一个连接。

        那主线程是发送的异步请求,那为什么还叫同步I/O呢?因为同步是指的负责读写I/O的线程。在子线程中,任务都是按顺序执行,因此,他是同步的。

        缺点:只适合连接数低的情况,当并发数量变高,因为每一个连接被分配一个线程,导致线程的的数量急剧增加,一秒内1万个请求就会需要1万个线程,且线程的切换开销也会变得巨大,导致服务器的性能急剧下降。

 NIO(Non-blocking I/O)同步非阻塞I/O​

  • 核心机制

        同步非阻塞:线程在读写时不会被阻塞,且单个线程可以管理多个网络连接,通过事件驱动机制高效处理I/O操作。

  • 工作原理

        首先,要了解这个设计模式的核心——三大组件        

        前面讲到BIO会为每一个连接分配线程,去处理读写操作,显然这是不合理的,因为让一个线程去处理一个连接,这是很浪费线程资源的,并且会造成线程的性能瓶颈。那要怎么解决这个问题?我们让一个线程去处理多个连接的读写不就可以了吗?这样既减少了线程的数量,也减少了线程切换的开销,不就提高了服务器的并发性?所以,在BIO中,我们用线程代表绑定连接,现在,我们用channel(通道)代表连接,去类比BIO中的线程分配。

        在BIO中,我们用主线程去监听端口获取连接事件,那么在NIO中,我们也用一个主channel去监听端口获取连接事件。每当由新的连接时,我们就为其分配一个子channel去负责这个连接的读写。但仅仅有channel够吗?不够,因为线程可以执行具体的任务,可以完成监听动作,读写动作,但channel不行,channel只是通道,负责数据的传输即读写。所以,我们还需要有监听动作,分发动作的执行者,这个时候,我们就会用到selector(选择器)去完成这些动作的执行

        我们在主线程创建一个Selector的实例,在创建主channel去绑定监听的端口。因为主channel仅仅只是监听连接事件的通道,不做业务处理,因此,他需要是非阻塞式的,也就是在创建完子channel后便继续监听连接。那么我们将这个selector注册到这个主channel中,并为他分配任务,去在这个通道里监听op_accept(连接事件)。

Selector selector = Selector.open();
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.bind(new InetSocketAddress(8080));
serverChannel.configureBlocking(false);
serverChannel.register(selector, SelectionKey.OP_ACCEPT); // 注册ACCEPT事件

        当selector监听到连接事件触发时,我们会为这个连接创建负责读写的channel,并为这个channel也注册selector负责监听写事件。那么监听到这个子通道的写事件时,我们也能做相应处理。注意,这里的selector与上文是同一个!!

          首先,我们要明白selector是动作的执行者,他就像一个公司的经理,是唯一的,在channel.register()这个方法中,我们看似是为channel注册了一个selector和分配给他的任务,实际上是selector将这个channel和这个任务事件绑定在了一起,用SelectionKey这个对象去表示他们的绑定,通过这个对象我们能获得他联系的channel和对应的监听事件。在注册的时候,底层会获得这个channel的文件描述符fd,通过系统调用告诉selector监听这个fd的任务事件。

        同时,我们写入读取channel中的数据的时候还需要buffer(缓冲区) 。     

        也就是在明白了这些,我们就可以基于NIO模型设计一个简单的服务端。如下:

public class NioServer {
    public static void main(String[] args) throws IOException {
        // 1. 创建Selector:用于监听多个通道的事件(如新连接、数据可读)
        Selector selector = Selector.open();

        // 2. 创建ServerSocketChannel监听端口,并配置为非阻塞模式
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.bind(new InetSocketAddress(8080)); // 绑定到8080端口
        serverChannel.configureBlocking(false); // 设置为非阻塞模式

        // 3. 将ServerSocketChannel注册到Selector,监听ACCEPT事件(新连接到达)
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        // 4. 创建线程池处理业务逻辑(避免阻塞Selector线程)
        ExecutorService workerPool = Executors.newFixedThreadPool(4);

        // 5. 事件循环:持续监听并处理事件
        while (true) {
            // 5.1 阻塞等待至少一个事件就绪(如新连接或数据可读)
            //在高并发的情况下,同一时间,selector可能监听到多个事件
            selector.select();

            // 5.2 获取所有就绪的事件集合
            Set selectedKeys = selector.selectedKeys();
            Iterator keyIterator = selectedKeys.iterator();

            // 5.3 遍历处理每个事件
            while (keyIterator.hasNext()) {
                SelectionKey key = keyIterator.next();
                keyIterator.remove(); // 必须移除已处理的Key,避免重复处理

                // 5.4 当监听到连接事件(ACCEPT事件)
                if (key.isAcceptable()) {
                    // 5.4.1 获取ServerSocketChannel(即触发事件的通道也就是主通道)
                    ServerSocketChannel server = (ServerSocketChannel) key.channel();

                    // 5.4.2 接受新连接,得到SocketChannel(客户端通道,子通道)
                    SocketChannel client = server.accept();
                    client.configureBlocking(false); // 设置为非阻塞模式

                    // 5.4.3 将客户端通道注册到Selector,监听READ事件(数据可读)
                    client.register(selector, SelectionKey.OP_READ);
                    System.out.println("新客户端连接: " + client.getRemoteAddress());

                // 5.5 当监听到读事件(READ事件)
                } else if (key.isReadable()) {
                    // 5.5.1 异步提交任务到线程池处理读事件
                    workerPool.submit(() -> {
                        SocketChannel client = (SocketChannel) key.channel();
                        try {
                            // 5.5.2 读取客户端数据
                            ByteBuffer buffer = ByteBuffer.allocate(1024);
                            int bytesRead = client.read(buffer);
                            if (bytesRead == -1) { // 客户端关闭连接
                                client.close();
                                return;
                            }

                            // 5.5.3 处理数据(模拟HTTP请求)
                            buffer.flip();
                            String request = new String(buffer.array(), 0, bytesRead);
                            System.out.println("收到请求:\n" + request);

                            // 5.5.4 生成HTTP响应
                            String response = "HTTP/1.1 200 OK\r\n\r\nHello, NIO!";
                            ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
                            client.write(responseBuffer); // 发送响应

                            // 5.5.5 关闭连接(示例为短连接)
                            client.close();
                        } catch (IOException e) {
                            e.printStackTrace();
                        }
                    });
                }
            }
        }
    }
}
  • 线程模型

        单线程模型:单个线程完成上述所有I/O操作。拥有高并发能力。但收到单核限制。性能存在瓶颈。

        多线程模型:Reactor模式

        Acceptor线程:只监听连接,将读写操作交给I/O线程。

        I/O线程:处理读写事件,可以利用线程池,一个I/O线程可以处理多个channel的读写。

注意:这里要区分NIO和BIO的线程的工作模式,在BIO下,单个线程绑定单个连接是阻塞的,也就是读不到数据,线程会一直等待,而NIO是轮询的,线程池中的线程只会处理已经就绪的事件,也就是说只有当有数据的时候才会有线程执行。

             

    你可能感兴趣的:(nio,java)