Entrada/Saída Não Bloqueante em Java

Fundamentos da API NIO

A API de Nova Entrada/Saída do Java (NIO) introduz um modelo de processamento de dados baseado em três pilares fundamentais: Canais (Channels), Áreas de Transferência (Buffers) e Multiplexadores (Selectors). Diferentemente da abordagem tradicional orientada a fluxos, essa arquitetura permite operações assíncronas e maior controle sobre a movimentação de dados.

O fluxo de dados segue um padrão bidirecional: informações transitam entre canais e áreas de transferência, enquanto um único encadeamento pode supervisionar múltiplos canais simultaneamente através do mecanismo de seleção.

Canais de Comunicação

Canais representam conexões abertas para operações de E/S, apresentando características distintivas em relação aos fluxos convencionais:

  • Operações duplas: leitura e escrita no mesmo canal
  • Execução assíncrona sem bloqueio
  • Interação obrigatória com áreas de transferência

As implementações principais incluem:

Tipo Função
FileChannel Manipulação de arquivos
DatagramChannel Comunicação UDP
SocketChannel Conexões TCP cliente
ServerSocketChannel Escuta de conexões TCP

Áreas de Transferência (Buffers)

Buffers funcionam como recipientes de memória contígua que intermediam a comunicação com canais. Cada buffer mantém três marcadores internos:

  • Capacidade (capacity): tamanho fixo da área alocada
  • Posição (position): local atual de leitura/escrita
  • Limite (limit): fronteira de acesso aos dados válidos

O ciclo de vida típico envolve quatro etapas:

  1. Preenchimento com dados
  2. Chamada a flip() para alternar modo
  3. Extração dos dados
  4. Reinicialização com clear() ou compact()
// Exemplo de leitura de arquivo
RandomAccessFile arquivo = new RandomAccessFile("dados.txt", "r");
FileChannel canal = arquivo.getChannel();

ByteBuffer area = ByteBuffer.allocate(64);
int lidos;

while ((lidos = canal.read(area)) != -1) {
    area.flip();
    
    while (area.hasRemaining()) {
        System.out.print((char) area.get());
    }
    
    area.clear();
}

arquivo.close();

Modos de Operação

O método flip() prepara o buffer para leitura reposicionando o marcador de posição. Para releitura dos mesmos dados, rewind() retorna ao início sem alterar o limite.

Quando parte dos dados permanece não processada, compact() desloca os elementos restantes para o início, preservando-os para a próxima iteração.

Multiplexadores (Selectors)

Selectors habilitam a gestão eficiente de múltiplos canais por um único encadeamento, reduzindo drasticamente o overhead de troca de contexto entre threads.

Criação e Registro

Selector multiplexador = Selector.open();

canal.configureBlocking(false);
SelectionKey chave = canal.register(multiplexador, SelectionKey.OP_READ);

Eventos monitoráveis:

Constante Descrição
OP_CONNECT Conexão estabelecida
OP_ACCEPT Conexão pendente de aceitação
OP_READ Dados disponíveis para leitura
OP_WRITE Canal pronto para escrita

Processamento de Eventos

while (ativo) {
    int prontos = multiplexador.select();
    if (prontos == 0) continue;
    
    Set<SelectionKey> chaves = multiplexador.selectedKeys();
    Iterator<SelectionKey> iterador = chaves.iterator();
    
    while (iterador.hasNext()) {
        SelectionKey chave = iterador.next();
        
        if (chave.isAcceptable()) {
            // Nova conexão
        } else if (chave.isReadable()) {
            // Dados disponíveis
        }
        
        iterador.remove();
    }
}

Canais de Soquete

Cliente TCP

SocketChannel cliente = SocketChannel.open();
cliente.connect(new InetSocketAddress("servidor", 8080));

ByteBuffer saida = ByteBuffer.wrap("mensagem".getBytes());
while (saida.hasRemaining()) {
    cliente.write(saida);
}

Servidor TCP

ServerSocketChannel servidor = ServerSocketChannel.open();
servidor.bind(new InetSocketAddress(8080));
servidor.configureBlocking(false);

while (true) {
    SocketChannel conexao = servidor.accept();
    if (conexao != null) {
        // Processar conexão
    }
}

Comparativo: NIO versus IO Tradicional

Aspecto IO (java.io) NIO (java.nio)
Orientação Fluxos sequenciais Buffers aleatórios
Bloqueio Sempre bloqueante Bloqueante ou não
Multiplexação Uma thread por conexão Múltiplas conexões por thread
Movimentação Unidirecional Bidirecional

Aplicação Completa: Sistema de Mensagens

Servidor

public class ServidorMensagens {
    private final ServerSocketChannel canalServidor;
    private final Selector seletor;
    private final ByteBuffer entrada = ByteBuffer.allocate(512);
    private final ByteBuffer saida = ByteBuffer.allocate(512);
    private static final Charset CODIFICACAO = StandardCharsets.UTF_8;

    public ServidorMensagens(int porta) throws IOException {
        canalServidor = ServerSocketChannel.open();
        canalServidor.bind(new InetSocketAddress(porta));
        canalServidor.configureBlocking(false);
        
        seletor = Selector.open();
        canalServidor.register(seletor, SelectionKey.OP_ACCEPT);
    }

    public void executar() throws IOException {
        System.out.println("Servidor iniciado na porta " + 
            canalServidor.socket().getLocalPort());
        
        while (true) {
            seletor.select();
            
            for (SelectionKey chave : seletor.selectedKeys()) {
                if (chave.isAcceptable()) {
                    aceitarConexao();
                } else if (chave.isReadable()) {
                    processarMensagem(chave);
                }
                seletor.selectedKeys().remove(chave);
            }
        }
    }

    private void aceitarConexao() throws IOException {
        SocketChannel cliente = canalServidor.accept();
        cliente.configureBlocking(false);
        cliente.register(seletor, SelectionKey.OP_READ);
        cliente.write(CODIFICACAO.encode("Bem-vindo ao servidor!"));
    }

    private void processarMensagem(SelectionKey chave) throws IOException {
        SocketChannel origem = (SocketChannel) chave.channel();
        StringBuilder mensagem = new StringBuilder();
        
        int lidos;
        while ((lidos = origem.read(entrada)) > 0) {
            entrada.flip();
            mensagem.append(CODIFICACAO.decode(entrada));
            entrada.clear();
        }
        
        if (lidos == -1) {
            origem.close();
            return;
        }
        
        System.out.println("Recebido: " + mensagem);
        
        saida.put(("Eco: " + mensagem).getBytes(CODIFICACAO));
        saida.flip();
        origem.write(saida);
        saida.clear();
    }

    public static void main(String[] args) throws IOException {
        new ServidorMensagens(5000).executar();
    }
}

Cliente

public class ClienteMensagens {
    private final SocketChannel canal;
    private final Selector seletor;
    private final ByteBuffer recepcao = ByteBuffer.allocate(512);
    private static final Charset CODIFICACAO = StandardCharsets.UTF_8;

    public ClienteMensagens(String host, int porta) throws IOException {
        canal = SocketChannel.open();
        canal.configureBlocking(false);
        canal.connect(new InetSocketAddress(host, porta));
        
        while (!canal.finishConnect()) {
            Thread.sleep(100);
        }
        
        seletor = Selector.open();
        canal.register(seletor, SelectionKey.OP_READ);
        
        new Thread(this::receber).start();
    }

    public void enviar(String texto) throws IOException {
        canal.write(CODIFICACAO.encode(texto));
    }

    private void receber() {
        try {
            while (true) {
                seletor.select();
                
                for (SelectionKey chave : seletor.selectedKeys()) {
                    if (chave.isReadable()) {
                        SocketChannel fonte = (SocketChannel) chave.channel();
                        StringBuilder resposta = new StringBuilder();
                        
                        while (fonte.read(recepcao) > 0) {
                            recepcao.flip();
                            resposta.append(CODIFICACAO.decode(recepcao));
                            recepcao.clear();
                        }
                        
                        System.out.println("Servidor: " + resposta);
                    }
                    seletor.selectedKeys().remove(chave);
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) throws Exception {
        ClienteMensagens cliente = new ClienteMensagens("localhost", 5000);
        Scanner leitor = new Scanner(System.in);
        
        while (leitor.hasNextLine()) {
            cliente.enviar(leitor.nextLine());
        }
    }
}

Tags: Java NIO SocketChannel ServerSocketChannel Selector ByteBuffer

Publicado em 8-16 05:57