Evolução dos Modelos de I/O em Java
A plataforma Java oferece suporte a três modelos distintos de programação de rede: BIO (Blocking I/O), NIO (Non-Blocking I/O) e AIO (Asynchronous I/O).
BIO - Blocking I/O
O modelo BIO representa a abordagem tradicional bloqueante. O servidor utiliza uma estrutura de um thread por conexão: sempre que um cliente estabelece conexão, o servidor cria um thread dedicado para processar aquela requisição. Isso pode gerar desperdício de recursos quando conexões permanecem ativas sem realizar operações.
NIO - Non-Blocking I/O
O NIO introduz um modelo síncrono mas não-bloqueante. Um único thread consegue gerenciar múltiplas conexões através de um multiplexador. As requisições de conexão são registradas em um seletor que verifica quais canais possuem dados disponíveis para processamento.
AIO - Asynchronous I/O
O AIO proporciona total异步nia. O sistema operacional executa a operação de I/O e notifica a aplicação quando concluída. Esta abordagem é ideal para aplicações com muitas conexões de longa duração.
Análise Detalhada do BIO
Conceitos Fundamentais
O BIO (Blocking I/O) utiliza as classes tradicionais do pacote java.io. O servidor segue o paradigma de uma conexão = um thread. Embora seja possível melhorar o desempenho com pools de threads, o modelo base permanece bloqueante.
Fluxo operacional:
- Servidor cria
ServerSockete registra porta - Aguarda conexões através do método
accept() - Para cada cliente, cria um thread dedicado
Implementação de Exemplo
Servidor:
public class Servidor {
public static void main(String[] args) throws IOException {
System.out.println("=== Servidor iniciado ===");
ServerSocket servidor = new ServerSocket(9999);
Socket canal = servidor.accept();
InputStream entrada = canal.getInputStream();
BufferedReader leitor = new BufferedReader(new InputStreamReader(entrada));
String mensagem;
while ((mensagem = leitor.readLine()) != null) {
System.out.println("Recebido: " + mensagem);
}
}
}
Cliente:
public class Cliente {
public static void main(String[] args) throws IOException {
System.out.println("=== Cliente iniciado ===");
Socket conexao = new Socket("127.0.0.1", 9999);
OutputStream saida = conexao.getOutputStream();
PrintStream fluxo = new PrintStream(saida);
fluxo.println("Olá, Servidor!");
fluxo.flush();
}
}
Uma característica importante: o servidor permanece bloqueado até que o cliente envie dados.
Abordagem Pseudo-Assíncrona
Esta técnica utiliza um pool de threads com fila de tarefas. Quando um cliente se conecta, o Socket é encapsulado em uma tarefa Runnable e enviado ao pool, evitando a criação de threads ilimitados.
Cliente:
public class Cliente {
public static void main(String[] args) {
try {
Socket socket = new Socket("127.0.0.1", 9999);
OutputStream os = socket.getOutputStream();
PrintStream ps = new PrintStream(os);
Scanner sc = new Scanner(System.in);
while (true) {
System.out.print("Digite: ");
String msg = sc.nextLine();
ps.println(msg);
ps.flush();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Gerenciador do Pool:
public class GerenciadorPool {
private ExecutorService executor;
public GerenciadorPool(int maxThreads, int tamanhoFila) {
executor = new ThreadPoolExecutor(3, maxThreads,
120, TimeUnit.SECONDS, new ArrayBlockingQueue<>(tamanhoFila));
}
public void executar(Runnable tarefa) {
executor.execute(tarefa);
}
}
Servidor:
public class Servidor {
public static void main(String[] args) {
try {
ServerSocket ss = new ServerSocket(9999);
GerenciadorPool pool = new GerenciadorPool(3, 10);
while (true) {
Socket socket = ss.accept();
Runnable tarefa = new ProcessadorSocket(socket);
pool.executar(tarefa);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public class ProcessadorSocket implements Runnable {
private Socket socket;
public ProcessadorSocket(Socket socket) {
this.socket = socket;
}
@Override
public void run() {
try {
InputStream is = socket.getInputStream();
BufferedReader br = new BufferedReader(new InputStreamReader(is));
String msg;
while ((msg = br.readLine()) != null) {
System.out.println("Recebido: " + msg);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
Limitações: Embora utilize threads, o modelo continua bloqueante. Se o processamento de mensagens for lento, novas conexões podem expirar.
Análise Detalhada do NIO
Visão Geral
O NIO (New I/O ou Non-Blocking I/O) foi introduzido no Java 1.4, oferecendo alternativas mais eficientes aos modelos tradicionais. Utiliza buffers orientados a canais, diferentemente das streams orientadas a bytes.
Componentes principais:
- Channel: Canal de comunicação bidirecional
- Buffer: Memória para armazenamento temporário de dados
- Selector: Multiplexador para múltiplos canais
Comparação entre BIO e NIO
| Característica | BIO | NIO |
|---|---|---|
| Orientação | Stream (fluxo) | Buffer (bloco) |
| Bloqueio | Bloqueante | Não-bloqueante |
| Gerenciamento | Um thread por conexão | Seletor com múltiplos canais |
Buffer - Conceitos Essenciais
O Buffer é um array de memória tipado com métodos convenientes para manipulação. Principais características:
- Capacity: Tamanho fixo da memória alocada
- Position: Índice atual para leitura/escrita
- Limit: Limite de dados utilizáveis
- Mark: Posição salva para restauração
Relação: 0 <= mark <= position <= limit <= capacity
Criação de buffers:
ByteBuffer buffer = ByteBuffer.allocate(1024);
Operações principais:
buffer.put(dados); // Escrever
buffer.flip(); // Alternar para modo leitura
buffer.get(); // Ler
buffer.clear(); // Limpar (manter dados)
Exemplo prático:
@Test
public void testeBuffer() {
ByteBuffer buffer = ByteBuffer.allocate(10);
System.out.println("Posição: " + buffer.position());
System.out.println("Limite: " + buffer.limit());
System.out.println("Capacidade: " + buffer.capacidade());
String texto = "mundo";
buffer.put(texto.getBytes());
System.out.println("Após escrita - Posição: " + buffer.position());
buffer.flip();
System.out.println("Após flip - Posição: " + buffer.position());
System.out.println("Após flip - Limite: " + buffer.limit());
char caractere = (char) buffer.get();
System.out.println("Caractere lido: " + caractere);
}
Buffers Diretos vs Não-Diretos
Buffers diretos utilizam memória nativa, off-heap, oferecendo melhor desempenho em operações I/O intensivas. Criados com allocateDirect(). Ideais para dados de longa duração com operações frequentes.
Canais (Channel)
Canais representam conexões com dispositivos de I/O. Diferentemente de streams, permitem leitura e escrita bidirecional.
Implementações disponíveis:
- FileChannel: Operações com arquivos
- SocketChannel: TCP cliente
- ServerSocketChannel: TCP servidor
- DatagramChannel: UDP
Exemplo de escrita em arquivo:
@Test
public void escreverArquivo() throws IOException {
FileOutputStream fos = new FileOutputStream("dados.txt");
FileChannel canal = fos.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.put("Olá, Mundo!".getBytes());
buffer.flip();
canal.write(buffer);
canal.close();
System.out.println("Dados gravados!");
}
Exemplo de leitura:
@Test
public void lerArquivo() throws IOException {
FileInputStream fis = new FileInputStream("dados.txt");
FileChannel canal = fis.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
canal.read(buffer);
buffer.flip();
String conteudo = new String(buffer.array(), 0, buffer.remaining());
System.out.println(conteudo);
}
Copia de arquivos com NIO:
@Test
public void copiarArquivo() throws IOException {
FileInputStream entrada = new FileInputStream("origem.txt");
FileOutputStream saida = new FileOutputStream("destino.txt");
FileChannel canalEntrada = entrada.getChannel();
FileChannel canalSaida = saida.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (true) {
buffer.clear();
int bytesLidos = canalEntrada.read(buffer);
if (bytesLidos == -1) break;
buffer.flip();
canalSaida.write(buffer);
}
canalEntrada.close();
canalSaida.close();
}
Scatter e Gather:
@Test
public void scatterGather() throws IOException {
FileInputStream fis = new FileInputStream("entrada.txt");
FileOutputStream fos = new FileOutputStream("saida.txt");
FileChannel canalEntrada = fis.getChannel();
FileChannel canalSaida = fos.getChannel();
ByteBuffer buffer1 = ByteBuffer.allocate(6);
ByteBuffer buffer2 = ByteBuffer.allocate(1024);
ByteBuffer[] buffers = {buffer1, buffer2};
canalEntrada.read(buffers);
for (ByteBuffer buf : buffers) {
buf.flip();
System.out.println(new String(buf.array(), 0, buf.remaining()));
}
canalSaida.write(buffers);
canalEntrada.close();
canalSaida.close();
}
Transferência direta entre canais:
canalSaida.transferFrom(canalEntrada, 0, canalEntrada.size());
// ou
canalEntrada.transferTo(0, canalEntrada.size(), canalSaida);
Seletores (Selector)
O Selector permite monitorar múltiplos canais com um único thread. Ideal para arquiteturas com muitas conexões simultâneas.
Registro de canais:
Selector seletor = Selector.open();
ServerSocketChannel canalServidor = ServerSocketChannel.open();
canalServidor.configureBlocking(false);
canalServidor.bind(new InetSocketAddress(9898));
canalServidor.register(seletor, SelectionKey.OP_ACCEPT);
Eventos de seleção:
OP_ACCEPT: Nova conexãoOP_READ: Dados disponíveis para leituraOP_WRITE: Pronto para escritaOP_CONNECT: Conexão estabelecida
Servidor Non-Blocking com NIO
public class ServidorNIO {
public static void main(String[] args) throws IOException {
ServerSocketChannel canalServidor = ServerSocketChannel.open();
canalServidor.configureBlocking(false);
canalServidor.bind(new InetSocketAddress(9999));
Selector seletor = Selector.open();
canalServidor.register(seletor, SelectionKey.OP_ACCEPT);
while (seletor.select() > 0) {
Iterator<SelectionKey> iterator = seletor.selectedKeys().iterator();
while (iterator.hasNext()) {
SelectionKey chave = iterator.next();
if (chave.isAcceptable()) {
SocketChannel canalCliente = canalServidor.accept();
canalCliente.configureBlocking(false);
canalCliente.register(seletor, SelectionKey.OP_READ);
} else if (chave.isReadable()) {
SocketChannel canal = (SocketChannel) chave.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int len = 0;
while ((len = canal.read(buffer)) > 0) {
buffer.flip();
System.out.println(new String(buffer.array(), 0, len));
buffer.clear();
}
}
iterator.remove();
}
}
}
}
Cliente Non-Blocking com NIO
public class ClienteNIO {
public static void main(String[] args) throws IOException {
SocketChannel canal = SocketChannel.open(new InetSocketAddress("127.0.0.1", 9999));
canal.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
Scanner scanner = new Scanner(System.in);
while (scanner.hasNext()) {
String texto = scanner.nextLine();
buffer.put((new Date().toString() + "\n" + texto).getBytes());
buffer.flip();
canal.write(buffer);
buffer.clear();
}
canal.close();
}
}
Introdução ao AIO
O AIO (Asynchronous I/O) resolve completamente a questão do bloqueo durante operações de I/O. Enquanto NIO ainda requer thread para processamento, AIO delega a operação ao sistema operacional.
Características:
- Totalmente assíncrono
- Callback automático após conclusão
- Disponível desde Java 7 (NIO.2)
Canais assíncronos:
AsynchronousSocketChannelAsynchronousServerSocketChannelAsynchronousFileChannelAsynchronousDatagramChannel
Exemplo de leitura assíncrona:
public class LeituraAssincrona {
public static void main(String[] args) throws IOException {
AsynchronousFileChannel canal =
AsynchronousFileChannel.open(Paths.get("dados.txt"), StandardOpenOption.READ);
ByteBuffer buffer = ByteBuffer.allocate(2);
System.out.println("Iniciando leitura...");
canal.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer resultado, ByteBuffer anexo) {
System.out.println("Leitura concluída! Bytes: " + resultado);
buffer.flip();
System.out.println("Dados: " + new String(buffer.array(), 0, buffer.remaining()));
}
@Override
public void failed(Throwable erro, ByteBuffer anexo) {
System.out.println("Erro na leitura: " + erro.getMessage());
}
});
System.out.println("Executando outras tarefas...");
System.in.read();
}
}
Observação: A leitura é processamento por outra thread, enquanto o thread principal contiuna livre.
Resumo Comparativo
| Modelo | Tipo | Thread por Conexão |
|---|---|---|
| BIO | Síncrono e Bloqueante | Sim (um por conexão) |
| NIO | Síncrono e Não-Bloqueante | Não (multiplexado) |
| AIO | Assíncrono e Não-Bloqueante | Não (SO来处理) |
Cenários de Uso
- BIO: Aplicações com poucas conexões fixas, requisitos simples
- NIO: Chat servers, aplicativos com muitas conexões curtas
- AIO: Processamento de arquivos grandes, operações pesadas
O modeloBIO é intuitivo porém limitado em escalabilidade. NIO oferece excelente performance com complexidade mdoerada. AIO proporciona máxima performance mas requer implementação mais elaborados e suporte do sistema operacional.