Desenvolvimento de Aplicações de Socket Cliente-Servidor em Java

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

Tags: java socket ServerSocket TCP/IP Network Programming

Publicado em 7-1 17:20