Detailed introduction of BIO, NIO, IO multiplexing model & Java NIO network programming

foreword

The basic knowledge of network programming is introduced above, and the network programming of BIO is written based on Java. We know that there are huge problems in the BIO model, such as the C10K problem, the essence of which is due to blocking reasons, so that if you want to bear more requests, you must have enough threads, but enough threads will cause memory usage problems , The performance problem caused by CPU context switching, which causes the server to crash. How to solve this problem? Optimization, so there will be NIO, AIO, and IO multiplexing later. This article will detail these models and write NIO based on Java.

basic concept

Where is I/O blocking blocked and how is it blocked? First understand some basic concepts

  • User space: The virtual address space allocated to the user process to store the code, data, and stack of the user process.
  • Kernel space: the basis of the operating system, responsible for managing the hardware resources of the computer and providing a system call interface, and also a bridge between user space and hardware.

insert image description here

In order to ensure the security and stability of the operating system, the user process and the operating system kernel are isolated. The user process cannot directly access the kernel space, but needs to initiate a request to the kernel through system calls, etc., and the kernel performs operations on behalf of the user process.

That is to say, our application needs to go through the kernel when reading or writing data to hardware devices, such as network cards and disks. The following introduces the BIO, NIO, and IO multiplexing models one by one, and learns about the IO process of each model in detail.

BIO process

First of all, let me make it clear that what we call IO blocking is the process that the user process, that is, the program in the user space, is reading from the hardware device. When there is no data, the feedback to the user needs to wait all the time. This is called blocking IO. . The process is as follows:
insert image description here

We can see that after the process initiates a call to the kernel until the data is returned, the whole process is blocked. Combined with Java BIO programming, that is to say, this inputStream.read()process is blocked, and there are several problems:

  1. Since blocking will occupy the current thread, making it unable to perform other operations, only new threads can be created when there is a new request. In the Linux system, the default stack size of each thread is 8MB. Without considering other factors, an 8G server can carry up to 1000 requests.
  2. Since the number of threads will increase as the amount of requests increases, when a large number of threads are blocked and woken up, frequent context switching by the CPU will lead to performance degradation.

This problem is the essence of the C10K problem. It seems very intuitive. Can it be solved by using fewer threads to handle multiple IOs? Continue to see the NIO process.

NIO process

NIO we are talking about non-blocking. Through the description of BIO, the non-blocking of NIO is reflected in: no matter whether there is data or not, it directly responds to the user process, as shown in the following figure:

insert image description here

We can see that it does recvfrom()respond directly after the user process calls the function, but it keeps polling and calling before getting the data. Although there is no CPU context switching due to blocking, the CPU is always in an idling state and cannot fully utilize the CPU. role. Like BIO, in the case of a single thread, IO events can only be processed sequentially, and a single thread still cannot handle multiple IO events.

IO multiplexing process

Since NIO, like BIO, cannot solve the C10K problem that may be caused by blocking, how can one thread handle multiple IO events? Can it be like this: use a thread to monitor these IOs, and receive data once any IO has data. IO multiplexing is this principle, as shown in the figure below:

insert image description here

We can see that there is one more select()function call, select()which will monitor the specified FD (note here, in Linux, everything is a file, including sockets), and the kernel will monitor the sockets corresponding to the FD. If any one or more sockets have data, they will be returned . At this time , the data of the receiving sockets select()will be called , so that a single thread can handle multiple I/O operations and improve the efficiency and performance of the system.recvfrom()

Under Linux, there are three commonly used I/O multiplexing methods: select, poll, and epoll.

  • The principle of select and poll is based on polling, that is, to continuously query all registered I/O events, and immediately notify the application if an event occurs. This method is inefficient because each query needs to traverse all I/O events.

  • The principle of epoll is based on event notification, that is, the application is notified only when an I/O event occurs. This approach is more efficient because it avoids invalid queries.

Java NIO programming

Compared with Java BIO programming, Java NIO programming is not so intuitive to understand, but it is relatively easy to understand after understanding multiple IO models (especially IO multiplexing). Java NIO is actually IO multiplexing.

Java NIO Core Concepts

In Java NIO programming, there are several core concepts (components) that need to be understood:

  • Channel (Channel): A channel is an abstraction of raw I/O operations and can be used to read and write data. It can interact with files, sockets, etc.

  • Buffer (Buffer): A buffer is a container for storing data. When performing read and write operations, data is first read into the buffer, and then written or read from the buffer.

  • Selector: Selector is a multiplexing mechanism provided by Java NIO, which can manage I/O operations of multiple channels through one thread.

Compared with BIO, developers do not directly interact with Socket, but provide methods to manage the capacity, location, and limit of the buffer by interacting with Selectormultiple Channelsockets . By setting these attributes, the location and range of reading and writing data can be controlled. BufferIn short, NIO supports richer functions while improving IO processing efficiency and performance.

Java NIO example

The following is a simple Java NIO network programming example for creating a NIO-based server and client:

Server code:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;

public class NIOServer {
    
    

    private Selector selector;

    public static void main(String[] args) throws IOException {
    
    
        NIOServer server = new NIOServer();
        server.startServer();
    }

    public void startServer() throws IOException {
    
    
        // 创建Selector
        selector = Selector.open();

        // 创建ServerSocketChannel,并绑定端口
        ServerSocketChannel serverChannel = ServerSocketChannel.open();
        serverChannel.configureBlocking(false);
        serverChannel.socket().bind(new InetSocketAddress(8888));

        // 将ServerSocketChannel注册到Selector上,并监听连接事件。当接收到一个客户端连接请求时就绪。该操作只给服务器使用。
        serverChannel.register(selector, SelectionKey.OP_ACCEPT);

        System.out.println("Server started on port 8888");

        // 循环等待事件发生
        while (true) {
    
    
            // 等待事件触发,阻塞 | selectNow():非阻塞,立刻返回。
            selector.select();

            Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
            while (keys.hasNext()) {
    
    
                SelectionKey key = keys.next();
                // 移除当前处理的SelectionKey
                keys.remove();

                if (key.isAcceptable()) {
    
    
                    // 处理连接请求
                    handleAccept(key);
                }

                if (key.isReadable()) {
    
    
                    // 处理读数据请求
                    handleRead(key);
                }
            }
        }
    }

    private void handleAccept(SelectionKey key) throws IOException {
    
    
        ServerSocketChannel serverChannel = (ServerSocketChannel) key.channel();
        // 监听到ServerSocketChannel连接事件,获取到连接的客户端
        SocketChannel clientChannel = serverChannel.accept();
        clientChannel.configureBlocking(false);
        // 将clientChannel注册到Selector上,并监听读事件,当操作系统读缓冲区有数据可读时就绪(该客户端的)。
        clientChannel.register(selector, SelectionKey.OP_READ);

        System.out.println("Client connected: " + clientChannel.getRemoteAddress());
    }

    private void handleRead(SelectionKey key) throws IOException {
    
    
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = clientChannel.read(buffer);

        if (bytesRead == -1) {
    
    
            // 客户端断开连接
            key.cancel();
            clientChannel.close();
            System.out.println("Client disconnected ");
            return;
        }

        byte[] data = new byte[bytesRead];
        buffer.flip();
        buffer.get(data);

        String message = new String(data).trim();
        System.out.println("Received message from client: " + message);

        // 回复客户端
        String response = "Server response: " + message;
        ByteBuffer responseBuffer = ByteBuffer.wrap(response.getBytes());
        clientChannel.write(responseBuffer);
    }
}

Client code:

import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Scanner;

public class NIOClient {
    
    
    private Selector selector;
    private SocketChannel socketChannel;

    public static void main(String[] args) {
    
    
        NIOClient client = new NIOClient();
        new Thread(() -> client.doConnect("localhost", 8888)).start();
        Scanner scanner = new Scanner(System.in);
        while (true) {
    
    
            String message = scanner.nextLine();
            if ("bye".equals(message)) {
    
    
                // 如果发送的消息是"bye",则关闭连接并退出循环
                client.doDisConnect();
                break;
            }
            client.sendMsg(message);
        }

    }

    private void doDisConnect() {
    
    
        try {
    
    
            socketChannel.close();
        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    private void sendMsg(String message) {
    
    
        // 发送消息到服务器
        ByteBuffer buffer = ByteBuffer.wrap(message.getBytes());
        try {
    
    
            socketChannel.write(buffer);

        } catch (IOException e) {
    
    
            e.printStackTrace();
        }
    }

    private void doConnect(String host, int port) {
    
    
        try {
    
    
            selector = Selector.open();
            // 创建SocketChannel并连接服务器
            socketChannel = SocketChannel.open();
            socketChannel.configureBlocking(false);
            socketChannel.connect(new InetSocketAddress(host, port));
            // 等待连接完成
            while (!socketChannel.finishConnect()) {
    
    
                // 连接未完成,可以做一些其他的事情
            }
            socketChannel.register(selector, SelectionKey.OP_READ);
            System.out.println("连接成功!");
            while (true) {
    
    
                // 等待事件触发,阻塞 | selectNow():非阻塞,立刻返回。
                selector.select();

                Iterator<SelectionKey> keys = selector.selectedKeys().iterator();
                while (keys.hasNext()) {
    
    
                    SelectionKey key = keys.next();
                    // 移除当前处理的SelectionKey
                    keys.remove();
                    if (key.isReadable()) {
    
    
                        // 处理读数据请求
                        handleRead(key);
                    }
                }
            }
        } catch (IOException e) {
    
    
            System.out.println("连接失败!!!");
            e.printStackTrace();
        }
    }

    private void handleRead(SelectionKey key) throws IOException {
    
    
        SocketChannel clientChannel = (SocketChannel) key.channel();
        ByteBuffer buffer = ByteBuffer.allocate(1024);
        int bytesRead = clientChannel.read(buffer);

        if (bytesRead == -1) {
    
    
            // 释放资源
            key.cancel();
            clientChannel.close();
            return;
        }

        byte[] data = new byte[bytesRead];
        buffer.flip();
        buffer.get(data);

        String message = new String(data).trim();
        System.out.println("Received message from server: " + message);
    }


}

Summarize

Through the introduction of this article, we can understand the principles of each IO model, and have a clearer understanding of many concepts, such as:
blocking is reflected in: after the user process initiates the system call interface, whether there is data or not, whether it directly responds to the result? If the direct response is non-blocking, waiting is blocking;
the principle of IO multiplexing is that a single thread processes multiple I/O operations, thereby improving the efficiency and performance of the system;
and through the understanding of IO multiplexing, a quick introduction Java NIO programming.

Guess you like

Origin blog.csdn.net/qq_28314431/article/details/132047951