A programação de rede, em sua essência, trata da comunicação de dados entre diferentes máquinas. Para um desenvolvedor Java, o Java Development Kit (JDK) oferece um conjunto robusto e simplificado de APIs no pacote java.net para facilitar essa tarefa. O conceito fundamental é o de sockets, que permitem o estabelecimento de canais de comunicação para a troca de informações.
O modelo básico para a maioria das aplicações de rede é o cliente-servidor. Neste paradigma, um processo, o servidor, atua como um ponto de escuta fixo, aguardando por solicitações. Outro processo, o cliente, inicia a conexão com o servidor, e uma vez estabelecida, ambos podem trocar dados. Em Java, o suporte a esse modelo é amplamente provido pela classe ServerSocket para o lado do servidor e pela classe Socket para o lado do cliente.
Configuração do Servidor com ServerSocket
Para criar um ponto de escuta em uma aplicação servidora, utilizamos a classe ServerSocket. Ao instanciá-la, é necessário especificar uma porta, que servirá como um identificador único para o serviço em questão naquela máquina. As portas variam de 0 a 65535, sendo as portas de 0 a 1023 reservadas para serviços bem conhecidos do sistema operacional (como HTTP na porta 80, FTP na 21, etc.). Recomenda-se utilizar portas acima de 1023 para aplicações personalizadas.
Exemplo de Instanciação do ServerSocket
import java.io.IOException;
import java.net.ServerSocket;
public class ServidorInicial {
public static void main(String[] args) throws IOException {
int portaServidor = 8888; // Escolha uma porta disponível
ServerSocket servidorSocket = new ServerSocket(portaServidor);
System.out.println("Servidor escutando na porta " + portaServidor);
// O servidor agora está pronto para aceitar conexões
}
}
Conectando com o Cliente via Socket
Do lado do cliente, a conexão com o servider é iniciada através da classe Socket. Para isso, o cliente precisa conhecer o endereço IP (ou nome do host) do servidor e a porta na qual o servidor está escutando. A classe InetAddress, também do pacote java.net, é utilizada para representar endereços IP, e pode ser obtida através de métodos estáticos para o host local ou para um host específico.
Exemplo de Conexão do Cliente
import java.io.IOException;
import java.net.InetAddress;
import java.net.Socket;
public class ClienteInicial {
public static void main(String[] args) throws IOException {
String enderecoServidor = "localhost"; // Ou o IP do servidor, ex: "127.0.0.1"
int portaServidor = 8888;
Socket clienteSocket = new Socket(InetAddress.getByName(enderecoServidor), portaServidor);
System.out.println("Cliente conectado ao servidor em " + enderecoServidor + ":" + portaServidor);
// O cliente agora possui um socket conectado
}
}
Transferência de Dados via Streams de E/S
Uma vez que a conexão entre cliente e servidor é estabelecida, a troca de dados é realizada por meio de fluxos de entrada e saída (I/O Streams), localizados no pacote java.io. Cada Socket oferece um InputStream para receber dados e um OutputStream para enviar dados. Para facilitar a manipulação de texto, é comum envolver esses streams de bytes em adaptadores de caracteres, como InputStreamReader e OutputStreamWriter, e depois em buffers como BufferedReader e PrintWriter.
Exemplo de Configuração de Streams
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.Socket;
public class ConfigStreams {
public static void configurar(Socket socketDeComunicacao) throws IOException {
BufferedReader leitorDeEntrada = new BufferedReader(
new InputStreamReader(socketDeComunicacao.getInputStream()));
PrintWriter escritorDeSaida = new PrintWriter(
socketDeComunicacao.getOutputStream(), true); // 'true' para autoFlush
System.out.println("Streams de entrada e saída configurados para o socket.");
// leitorDeEntrada e escritorDeSaida podem agora ser usados para trocar mensagens de texto.
}
}
Exemplo Completo: Aplicação de Eco Simples (Um Cliente)
Vamos criar um par cliente-servidor que implementa um serviço de "eco". O cliente envia uma mensagem, e o servidor responde com a mesma mensagem (ou uma confirmação de recebimento). A comunicação continua até que o cliente envie a palavra "fim".
Servidor de Eco Simples
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
public class ServidorEcoUnico {
public static void main(String[] args) throws IOException {
int portaServidor = 8888;
ServerSocket servidorDeEscuta = new ServerSocket(portaServidor);
System.out.println("Servidor de Eco aguardando conexões na porta " + portaServidor + "...");
try (Socket conexaoCliente = servidorDeEscuta.accept()) { // Aguarda uma conexão
System.out.println("Cliente conectado de: " + conexaoCliente.getInetAddress().getHostAddress());
BufferedReader entradaCliente = new BufferedReader(
new InputStreamReader(conexaoCliente.getInputStream()));
PrintWriter saidaCliente = new PrintWriter(
conexaoCliente.getOutputStream(), true); // Auto-flush
String mensagemRecebida;
while ((mensagemRecebida = entradaCliente.readLine()) != null) {
System.out.println("Recebido do cliente: " + mensagemRecebida);
saidaCliente.println("Servidor: Recebi sua mensagem: '" + mensagemRecebida + "'");
if (mensagemRecebida.equalsIgnoreCase("fim")) {
break;
}
}
System.out.println("Conexão com o cliente encerrada.");
} finally {
servidorDeEscuta.close();
System.out.println("Servidor encerrado.");
}
}
}
Cliente de Eco Simples
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.InetAddress;
import java.net.Socket;
public class ClienteEcoUnico {
public static void main(String[] args) throws IOException {
String hostServidor = "localhost";
int portaServidor = 8888;
try (Socket socketServidor = new Socket(InetAddress.getByName(hostServidor), portaServidor);
BufferedReader consoleInput = new BufferedReader(new InputStreamReader(System.in));
BufferedReader entradaServidor = new BufferedReader(new InputStreamReader(socketServidor.getInputStream()));
PrintWriter saidaServidor = new PrintWriter(socketServidor.getOutputStream(), true)) {
System.out.println("Conectado ao Servidor de Eco em " + hostServidor + ":" + portaServidor);
System.out.println("Digite mensagens (ou 'fim' para sair):");
String mensagemUsuario;
while ((mensagemUsuario = consoleInput.readLine()) != null) {
saidaServidor.println(mensagemUsuario); // Envia para o servidor
System.out.println(entradaServidor.readLine()); // Recebe e imprime a resposta do servidor
if (mensagemUsuario.equalsIgnoreCase("fim")) {
break;
}
}
System.out.println("Conexão com o servidor encerrada.");
} catch (IOException e) {
System.err.println("Erro de comunicação: " + e.getMessage());
}
}
}
Lidando com Múltiplos Clientes Concorrentes
O servidor de eco anterior gerencia apenas um cliente por vez. Se um segundo cliente tentar se conectar enquanto o primeiro ainda está ativo, ele terá que esperar. Para que um servidor possa atender múltiplos clientes simultaneamente, é necessário empregar concorrência, e o mecanismo mais comum em Java para isso são as threads.
A abordagem é simples: quando o ServerSocket aceita uma nova conexão (serverSocket.accept()), ele retorna um novo Socket para essa conexão. Em vez de o thread principal do servidor lidar diretamente com essa comunicação, ele delega essa tarefa a um novo thread, que será responsável por toda a interação com aquele cliente específico. O thread principal então volta a aguardar por novas conexões.
Servidor de Eco Multi-Cliente com Threads
Vamos criar uma classe ManipuladorDeCliente que implementa a interface Runnable. Cada instância dessa classe encapsulará a lógica de comunicação com um cliente individual e será executada em seu próprio thread.
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
// Classe responsável por gerenciar a comunicação com um único cliente
class ManipuladorDeCliente implements Runnable {
private Socket socketCliente;
public ManipuladorDeCliente(Socket socket) {
this.socketCliente = socket;
}
@Override
public void run() {
try (
BufferedReader entrada = new BufferedReader(new InputStreamReader(socketCliente.getInputStream()));
PrintWriter saida = new PrintWriter(socketCliente.getOutputStream(), true)
) {
String mensagemDoCliente;
System.out.println("Iniciando comunicação com cliente: " + socketCliente.getInetAddress().getHostAddress());
while ((mensagemDoCliente = entrada.readLine()) != null) {
System.out.println("Cliente " + socketCliente.getInetAddress().getHostAddress() + " enviou: " + mensagemDoCliente);
saida.println("Servidor: Recebi e ecoei: '" + mensagemDoCliente + "'");
if (mensagemDoCliente.equalsIgnoreCase("sair")) {
System.out.println("Cliente " + socketCliente.getInetAddress().getHostAddress() + " solicitou encerramento.");
break;
}
}
} catch (IOException e) {
System.err.println("Erro de E/S no manipulador de cliente " + socketCliente.getInetAddress().getHostAddress() + ": " + e.getMessage());
} finally {
try {
socketCliente.close();
System.out.println("Conexão com cliente " + socketCliente.getInetAddress().getHostAddress() + " encerrada.");
} catch (IOException e) {
System.err.println("Erro ao fechar socket do cliente: " + e.getMessage());
}
}
}
}
public class ServidorMultiCliente {
public static void main(String[] args) {
int portaServidor = 8888;
try (ServerSocket serverSocketPrincipal = new ServerSocket(portaServidor)) {
System.out.println("Servidor multi-cliente iniciado e escutando na porta " + portaServidor + "...");
while (true) { // Loop infinito para aceitar novas conexões
Socket novoClienteSocket = serverSocketPrincipal.accept(); // Bloqueia até que uma conexão seja aceita
System.out.println("Nova conexão aceita de: " + novoClienteSocket.getInetAddress().getHostAddress());
// Cria um novo thread para lidar com este cliente
Thread threadCliente = new Thread(new ManipuladorDeCliente(novoClienteSocket));
threadCliente.start(); // Inicia o thread
}
} catch (IOException e) {
System.err.println("Erro fatal no servidor: " + e.getMessage());
e.printStackTrace();
}
}
}