Introdução detalhada do modelo de multiplexação BIO, NIO, IO e programação de rede Java NIO

prefácio

O conhecimento básico de programação de rede é apresentado acima, e a programação de rede do BIO é escrita com base em Java. Sabemos que existem grandes problemas no modelo BIO, como o problema C10K, cuja essência se deve a motivos de bloqueio, de modo que se você deseja suportar mais solicitações, deve ter threads suficientes, mas threads suficientes causarão memória problemas de uso , O problema de desempenho causado pela troca de contexto da CPU, que faz com que o servidor trave. Como resolver este problema? Otimização, então haverá multiplexação NIO, AIO e IO posteriormente. Este artigo detalhará esses modelos e escreverá NIO com base em Java.

conceito básico

Onde o bloqueio de E/S é bloqueado e como é bloqueado? Primeiro entenda alguns conceitos básicos

  • Espaço do usuário: O espaço de endereço virtual alocado ao processo do usuário para armazenar o código, os dados e a pilha do processo do usuário.
  • Espaço do kernel: a base do sistema operacional, responsável por gerenciar os recursos de hardware do computador e fornecer uma interface de chamada do sistema, e também uma ponte entre o espaço do usuário e o hardware.

insira a descrição da imagem aqui

Para garantir a segurança e a estabilidade do sistema operacional, o processo do usuário e o kernel do sistema operacional são isolados. O processo do usuário não pode acessar diretamente o espaço do kernel, mas precisa iniciar uma solicitação ao kernel por meio de chamadas do sistema, etc., e o kernel executa operações em nome do processo do usuário.

Ou seja, nosso aplicativo precisa passar pelo kernel ao ler ou gravar dados em dispositivos de hardware, como placas de rede e discos. A seguir apresenta os modelos de multiplexação BIO, NIO e IO, um por um, e aprende sobre o processo IO de cada modelo em detalhes.

processo BIO

Antes de tudo, deixe-me esclarecer que o que chamamos de bloqueio de IO é o processo que o processo do usuário, ou seja, o programa no espaço do usuário, está lendo do dispositivo de hardware. Quando não há dados, o feedback para o o usuário precisa esperar o tempo todo. Isso é chamado de bloqueio IO. O processo é como se segue:
insira a descrição da imagem aqui

Podemos ver que após o processo iniciar uma chamada ao kernel até que os dados sejam devolvidos, todo o processo é bloqueado. Combinado com a programação Java BIO, ou seja, esse processo é bloqueado, inputStream.read()e existem vários problemas:

  1. Como o bloqueio irá ocupar a thread atual, impossibilitando-a de realizar outras operações, somente novas threads poderão ser criadas quando houver uma nova requisição. No sistema Linux, o tamanho padrão da pilha de cada thread é de 8 MB, sem considerar outros fatores, um servidor 8G pode transportar até 1000 requisições.
  2. Uma vez que o número de threads aumentará à medida que a quantidade de solicitações aumenta, quando um grande número de threads é bloqueado e ativado, a troca frequente de contexto pela CPU levará à degradação do desempenho.

Esse problema é a essência do problema C10K. Parece muito intuitivo. Pode ser resolvido usando menos threads para lidar com vários IOs? Continue a ver o processo NIO.

processo NIO

NIO estamos falando de non-blocking. Através da descrição do BIO, o non-blocking do NIO se reflete em: não importa se há dados ou não, ele responde diretamente ao processo do usuário, conforme mostrado na figura a seguir:

insira a descrição da imagem aqui

Podemos ver que ele recvfrom()responde diretamente após o processo do usuário chamar a função, mas continua pesquisando e chamando antes de obter os dados. Embora não haja troca de contexto da CPU devido ao bloqueio, a CPU está sempre em estado ocioso e não pode utilizar totalmente o papel da CPU. Como o BIO, no caso de um único thread, os eventos de IO só podem ser processados ​​sequencialmente e um único thread ainda não pode lidar com vários eventos de IO.

processo de multiplexação IO

Como o NIO, como o BIO, não pode resolver o problema C10K que pode ser causado pelo bloqueio, como um thread pode lidar com vários eventos de IO? Pode ser assim: use um thread para monitorar esses IOs e receba dados assim que qualquer IO tiver dados. A multiplexação IO é este princípio, conforme mostrado na figura abaixo:

insira a descrição da imagem aqui

Podemos ver que existe mais uma select()chamada de função, select()que irá monitorar o FD especificado (observe aqui, no Linux, tudo é arquivo, inclusive os sockets), e o kernel irá monitorar os sockets correspondentes ao FD. Se algum ou mais sockets tiverem dados, eles serão retornados , neste momento , os dados dos sockets receptores select()serão chamados , para que uma única thread possa lidar com múltiplas operações de I/O e melhorar a eficiência e performance do sistema.recvfrom()

No Linux, existem três métodos de multiplexação de E/S comumente usados: select, poll e epoll.

  • O princípio de seleção e votação é baseado em votação, ou seja, consultar continuamente todos os eventos de E/S registrados e notificar imediatamente o aplicativo se ocorrer um evento. Esse método é ineficiente porque cada consulta precisa percorrer todos os eventos de E/S.

  • O princípio do epoll é baseado na notificação de evento, ou seja, a aplicação é notificada somente quando ocorre um evento de I/O. Essa abordagem é mais eficiente porque evita consultas inválidas.

Programação Java NIO

Em comparação com a programação Java BIO, a programação Java NIO não é tão intuitiva de entender, mas é relativamente fácil de entender depois de entender vários modelos IO (especialmente multiplexação IO).Java NIO é, na verdade, multiplexação IO.

Conceitos principais do Java NIO

Na programação Java NIO, existem vários conceitos básicos (componentes) que precisam ser compreendidos:

  • Canal (Channel): Um canal é uma abstração de operações de E/S brutas e pode ser usado para ler e gravar dados. Ele pode interagir com arquivos, soquetes, etc.

  • Buffer (Buffer): Um buffer é um recipiente para armazenar dados. Ao executar operações de leitura e gravação, os dados são primeiro lidos no buffer e, em seguida, gravados ou lidos do buffer.

  • Seletor: O seletor é um mecanismo de multiplexação fornecido pelo Java NIO, que pode gerenciar operações de E/S de vários canais por meio de um thread.

Em comparação com o BIO, os desenvolvedores não interagem diretamente com o Socket, mas fornecem métodos para gerenciar a capacidade, a localização e o limite do buffer interagindo com Selectorvários Channelsockets . Ao definir esses atributos, o local e o intervalo dos dados de leitura e gravação podem ser controlados . BufferResumindo, o NIO suporta funções mais ricas enquanto melhora a eficiência e o desempenho do processamento de IO.

Exemplo Java NIO

Veja a seguir um exemplo simples de programação de rede Java NIO para criar um servidor e cliente baseados em NIO:

Código do servidor:

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);
    }
}

Código do cliente:

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);
    }


}

Resumir

Através da introdução deste artigo, podemos entender os princípios de cada modelo de IO, e ter uma compreensão mais clara de muitos conceitos, como:
bloqueio é refletido em: após o processo do usuário iniciar a interface de chamada do sistema, se há dados ou não , se ele responde diretamente ao resultado? Se a resposta direta for sem bloqueio, a espera é bloqueada;
o princípio da multiplexação de IO é que um único thread processa várias operações de I/O, melhorando assim a eficiência e o desempenho do sistema;
e através da compreensão da multiplexação de IO, uma rápida introdução à programação Java NIO.

Acho que você gosta

Origin blog.csdn.net/qq_28314431/article/details/132047951
Recomendado
Clasificación