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:
- Preenchimento com dados
- Chamada a
flip()para alternar modo - Extração dos dados
- Reinicialização com
clear()oucompact()
// 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());
}
}
}