Entendendo protocolos TCP em Java.
Prefácio
Entender como funcionam as redes de computadores é entender toda a base que rege o funcionamento do mundo contemporâneo. As redes de internet fazem parte de um componente motor essencial para quase todas as operações nas quais são realizadas em computadores; desde usar um gerenciador de pacotes dentro do seu SO, ou baixar uma imagem Docker do Docker Hub, tudo isso depende da conexão entre um cliente e um servidor.
Universidades com seus sistemas acadêmicos que lidam com milhares de requisições simultâneas de alunos, sistemas de chats como o WhatsApp que permitem a comunicação entre pessoas ou comércios, redes sociais como o Reddit que funcionam em uma dinâmica de comunidades que se retroalimentam, e por aí vai…
Redes de computadores
Primeiramente precisamos definir alguns termos, para assim começarmos a prosseguir com alguns entendimento sobre como redes funcionam. Aqui vai um breve dicionário de termos que são bastante usados no estudo de redes:
- Host (sistemas finais/hospedeiros): aquele que é responsável por hospedar algum tipo de serviço
- Camada de enlace: é um grupo de métodos e protocolos de comunicação confinados dentro do link ao qual um hospedeiro está conectado fisicamente
- Links (enlaces): é o componente fisico e lógico usado para conectar dois computadores
- Switch (comutador): dispositivo que é responsável por conectar diversos dispositivos em uma rede LAN (rede local) usando endereços MAC (endereço físico gravado durante a fabricação do dispositivo)
- ISP (Internet Service Providers): o responsável por prover o serviço de internet.
- API (Application Programming Interface): um conjunto de regras que são estabelecidas arbitrariamente pelo desenvolvedor do sistema, e que devem ser seguidas por quem quer se comunicar com esse serviço
- Socket: é uma interface entre a camada de aplicação e a de transporte dentro de um host, geralmente chamada de API.
Protocolos de rede
Todas as atividades dentro da internet envolvem pelo menos a comunicação entre dois pontos, que devem seguir um protocolo para se comunicarem. Por exemplo: ao acessar o site “google.com” você está fazendo uma requisição GET dentro do protocolo HTTP, onde o servidor é o responsável por devolver ao usuário a página que foi requisitada na URL.
Arquiteturas de aplicação
A arquitetura de aplicação é uma forma arbitrária, decidida pelo programador, de compôr um sistema final. Temos alguns tipos bem conhecidos de arquiteturas de aplicação:
- Cliente Servidor: sempre há um hospedeiro em funcionamento, que fica responsável por responder aos pacotes dos clientes, ela tem um endereço fixo, geralmente é usada por websites.
- P2P (Peer 2 Peer): caracterizada pela descentralização dos hosts, utiliza a comunicação direta entre pares de hospedeiros. Como os pares se comunicam, não há necessidade de passar por um servidor dedicado, ou seja, é auto escalável.
TCP
Dentro deste post falarei mais sobre o protocolo TCP, pois envolve a dinâmica que rege o funcionamento atual da web, o TCP/IP. O protocolo TCP é resumido em uma forma confiável e íntegra de transferência de dados via pacotes.
Além de depender de sockets para haver uma comunicação, o TCP controla dentro do próprio protocolo a segmentação, fluxo e garante que tudo foi entregue na ordem como deveria.
TCP/IP
Como foi posto anteriormente, o TCP/IP estabelece uma junção dos dois protocolos, sendo o IP o responsável por definir o endereço no qual o pacote deve ser enviado, enquanto o TCP fica responsável por enviar e garantir a integridade desses pacotes.
Esse modelo representa a forma na qual os dados são passados entre redes, podendo essas serem de longa distância, ele é dividido em 4 camadas que explicam mais a fundo o funcionamento dessa comunicação:
- datalink: essa define como os dados serão enviados, de forma chula, é a camada de link, camada física e camada de acesso a rede.
- internet: responsável por enviar e controlar os pacotes, para que cheguem de forma correta ao seu destinatário, essa é a camada que divide e segmenta os dados em pacotes.
- transporte: fornece uma conexão sólida e confiável de dados entre quem envia e o destinatário, ela define quem envia, para onde, quantos dados devem ser enviados e em qual taxa devem ser mandados.
- aplicativo: refere-se a programas que precisam desse protocolo para a comunicação, normalmente o usuário interage com essa parte.
Dentro do TCP/IP os pacotes não são privados, ou seja, podem ser interceptados por um terceiro com acesso a rede, portanto é muito necessário uma camada de criptografia para proteger a mensagem em translado.
Exemplo de Echo em Java
Aqui coloco um projeto desenvolvido em Java para colocar em práticas conceitos de TCP, basicamente um servidor que ecoa a mensagem entre um cliente e servidor. Essa implementação é multi-thread, porém síncrona, portanto, suporte múltiplas conexões e responde apenas uma por vez, em fila.
Obs: optei por manter a documentação que foi feita pro javadoc, para evitar reescrever o que cada classe faz.
Cliente
/**
* Implementação de um cliente TCP Echo.
* <p>
* Esta classe permite que o usuário se conecte a um servidor, envie mensagens
* do console ({@code stdin}) e exiba as mensagens de eco recebidas do servidor.
* Ela também mede e exibe a latência de cada requisição.
* </p>
*/
public class TcpEchoClientImpl implements TcpEchoClient {
/**
* Inicia o cliente, conecta-se ao servidor e gerencia a comunicação de eco.
*
* @param ip O endereço IP do servidor para se conectar.
* @param port A porta do servidor.
* @throws ConnectionException se ocorrer um erro durante a conexão ou comunicação.
*/
@Override
public void start(String ip, int port) {
try (
var clientSocket = new Socket(ip, port);
var in = new BufferedReader(new InputStreamReader(clientSocket.getInputStream(), StandardCharsets.UTF_8));
var out = new PrintWriter(clientSocket.getOutputStream(), true, StandardCharsets.UTF_8);
var stdin = new BufferedReader(new InputStreamReader(System.in, StandardCharsets.UTF_8))
) {
System.out.println("\nConectado ao host: " + ip + ":" + port);
System.out.println("Servidor: " + in.readLine());
System.out.println("Servidor: " + in.readLine());
String line;
while ((line = stdin.readLine()) != null) {
var ini = System.nanoTime();
/*
* --> Instruções do projeto
* Protocolo de comunicação: cada mensagem deve ser finalizada com um '\n'
*/
out.write(line + "\n");
out.flush();
var resp = in.readLine();
if(resp == null || resp.contains("Tempo limite de inatividade atingido")) {
System.out.println("\nServidor: " + resp);
System.out.println("Servidor encerrou conexão.");
break;
}
var fim = System.nanoTime();
if("quit".equalsIgnoreCase(line)) {
System.out.println("Fechando conexão.");
break;
}
System.out.println("\nServidor: " + resp);
double latenciaMs = (double) (fim - ini) / 1_000_000.0;
System.out.printf("Latência: %.3f ms\n\n", latenciaMs);
}
} catch (IOException e) {
throw new ConnectionException("Erro ao realizar conexão", e);
} catch (Exception e) {
throw new ConnectionException("Erro inesperado ao realizar conexão", e);
}
}
}
Servidor
/**
* Implementação de um servidor TCP Echo multithreaded.
* <p>
* Este servidor aceita múltiplas conexões de clientes. Ele usa uma {@link java.util.concurrent.BlockingQueue}
* para gerenciar as conexões recebidas e as processa em uma thread separada,
* permitindo que o servidor continue aceitando novas conexões rapidamente.
* </p>
*/
public class TcpEchoServerImpl implements TcpEchoServer {
private final BlockingQueue<Socket> connectionQueue = new LinkedBlockingQueue<>();
private volatile boolean running = true;
/**
* Inicia o servidor em uma porta especificada.
* O servidor aceita novas conexões em um loop e as adiciona à fila de processamento.
* Uma thread separada é responsável por processar as conexões da fila.
*
* @param port A porta na qual o servidor irá escutar.
* @throws ConnectionException se ocorrer um erro ao iniciar o servidor ou aceitar uma conexão.
*/
@Override
public void start(int port) {
try (var serverSocket = new ServerSocket(port)) {
System.out.println("\nServidor conectado na porta: " + port);
new Thread(this::processConnections, "ConnectionProcessor").start();
while (running) {
var socket = serverSocket.accept();
connectionQueue.put(socket);
System.out.println("\nNova conexão aceita: " + socket.getRemoteSocketAddress());
sendQueuedMessage(socket);
}
} catch (IOException e) {
throw new ConnectionException("Erro ao aceitar nova conexão", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
System.err.println("Servidor interrompido.");
}
}
/**
* Envia uma mensagem inicial ao cliente informando que a conexão foi enfileirada.
*
* @param socket O socket do cliente para o qual a mensagem será enviada.
* @throws ConnectionException se ocorrer um erro de I/O ao enviar a mensagem.
*/
private void sendQueuedMessage(Socket socket) {
try {
var out = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8);
out.println("Sua conexão foi enfileirada. Aguarde para ser atendido.");
} catch (IOException e) {
throw new ConnectionException("Erro ao enviar mensagem inicial: ", e);
}
}
/**
* Método executado em uma thread separada para processar as conexões da fila.
* Este método pega uma conexão da fila, atende o cliente e o ecoa as mensagens.
* Além disso, trata de tempos de inatividade (com um timeout de 10 segundos),
* encerrando a conexão se o cliente não enviar dados dentro desse intervalo.
*/
private void processConnections() {
while (running) {
try (var socket = connectionQueue.take();
var in = new BufferedReader(new InputStreamReader(socket.getInputStream(), StandardCharsets.UTF_8));
var out = new PrintWriter(socket.getOutputStream(), true, StandardCharsets.UTF_8)) {
socket.setSoTimeout(10_000);
System.out.println("\nAtendendo cliente: " + socket.getRemoteSocketAddress() + "\n");
out.println("O servidor está pronto para processar sua conexão.");
String line;
while (true) {
try {
line = in.readLine();
if (line == null) {
System.out.println("\nConexão encerrada pelo cliente: " + socket.getRemoteSocketAddress());
break;
}
if("quit".equalsIgnoreCase(line)) {
System.out.println("\nConexão finalizada pelo cliente.");
break;
}
System.out.println("Mensagem recebida: " + line);
/*
* --> Instruções do projeto
* Protocolo de comunicação: cada mensagem deve ser finalizada com um '\n'
*/
out.write(line + "\n");
out.flush();
} catch (SocketTimeoutException ste) {
out.write("Tempo limite de inatividade atingido (10s). Encerrando conexão." + "\n");
out.flush();
System.out.println("Conexão encerrada: cliente inativo -> " + socket.getRemoteSocketAddress());
break;
}
}
} catch (IOException e) {
throw new ConnectionException("Erro ao aceitar nova conexão", e);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new ConnectionException("Processamento interrompido", e);
}
}
}
/**
* Sinaliza para o servidor que ele deve parar a execução.
*/
public void stop() {
running = false;
System.out.println("Servidor será finalizado...");
}
}
Application starter
/**
* Orquestra o início da aplicação, validando os argumentos
* da linha de comando e iniciando o modo de execução correto (cliente ou servidor).
* <p>
* Esta classe centraliza a lógica de validação de flags, IP e porta,
* garantindo que a aplicação seja executada de forma segura e com os
* parâmetros esperados.
* </p>
*/
public class ApplicationStarter {
private final FlagParser parser;
private final TcpEchoServer server;
private final TcpEchoClient client;
/**
* Constrói uma nova instância do {@code ApplicationStarter}.
*
* @param parser O analisador de flags para processar os argumentos da linha de comando.
* @param client O cliente TCP a ser iniciado, se o modo cliente for selecionado.
* @param server O servidor TCP a ser iniciado, se o modo servidor for selecionado.
*/
public ApplicationStarter(FlagParser parser, TcpEchoClient client, TcpEchoServer server) {
this.client = client;
this.server = server;
this.parser = parser;
}
/**
* Executa a lógica de inicialização da aplicação.
* <p>
* Este método valida os argumentos fornecidos e, em seguida, inicia
* o servidor ou o cliente com base nas flags `--server` ou `--client`.
* </p>
*
* @throws ConnectionException se as flags de modo estiverem ausentes ou duplicadas,
* ou se os argumentos de IP ou porta forem inválidos.
*/
public void execute() {
validateFlags();
if(parser.has("server")) {
int port = validatePort();
server.start(port);
}
if(parser.has("client")) {
String ip = validateIp();
int port = validatePort();
client.start(ip, port);
}
}
/**
* Valida as flags de modo de execução (`--server` e `--client`).
* <p>
* Garante que exatamente uma dessas flags esteja presente nos argumentos.
* </p>
*
* @throws ConnectionException se ambas as flags estiverem presentes, ou se nenhuma
* delas for encontrada.
*/
private void validateFlags() {
if(parser.has("server") && parser.has("client")) {
throw new ConnectionException("Cliente e servidor devem ser executados separadamente: ", new RuntimeException());
}
if(!parser.has("server") && !parser.has("client")) {
throw new ConnectionException("Servidor ou cliente devem ser declarados: ", new RuntimeException());
}
}
/**
* Valida a porta fornecida nos argumentos.
*
* @return O número da porta validado.
* @throws ConnectionException se o valor da porta não for um número inteiro válido
* ou estiver fora do intervalo de 1 a 65536.
*/
private int validatePort() {
int port = Integer.valueOf(parser.get("port"));
if(1 > port || port > 65536) {
throw new ConnectionException("A porta deve ser entre 1 e 65.536: ", new RuntimeException());
}
return port;
}
/**
* Valida o endereço IP fornecido nos argumentos.
*
* @return O endereço IP validado.
* @throws ConnectionException se o IP for nulo ou estiver vazio.
*/
private String validateIp() {
String ip = parser.get("ip");
if(ip == null || ip.trim().isEmpty()) {
throw new ConnectionException("O IP deve ser preenchido: ", new RuntimeException());
}
return ip;
}
}
Flag parser
/**
* Analisa argumentos da linha de comando formatados como flags.
* <p>
* Esta classe processa um array de strings, identificando argumentos
* que começam com "--" e os armazena em um mapa. As flags podem ser
* booleanas (ex: {@code --help}) ou ter um valor (ex: {@code --port=8080}).
* </p>
* <b>Formato Esperado:</b>
* <ul>
* <li>{@code --flag} (valor padrão "true")</li>
* <li>{@code --flag=valor}</li>
* </ul>
*/
public class FlagParser {
private final Map<String, String> flags = new HashMap<>();
/**
* Construtor que processa os argumentos da linha de comando.
*
* @param args O array de strings de argumentos da linha de comando.
*/
public FlagParser(String[] args) {
for(String arg: args) {
if(arg.startsWith("--")) {
String[] part = arg.substring(2).split("=",2);
if(part.length == 2) {
flags.put(part[0], part[1]);
} else {
flags.put(part[0], "true");
}
}
}
}
/**
* Obtém o valor de uma flag específica.
*
* @param key A chave (nome) da flag.
* @return O valor da flag como uma string, ou {@code null} se a flag não existir.
*/
public String get(String key) {
return flags.get(key);
}
/**
* Verifica se uma flag específica está presente nos argumentos.
*
* @param key A chave (nome) da flag a ser verificada.
* @return {@code true} se a flag estiver presente, caso contrário {@code false}.
*/
public boolean has(String key) {
return flags.containsKey(key);
}
}
Connection exception
/**
* Exceção de tempo de execução personalizada para erros de conexão.
* <p>
* Esta exceção é usada para encapsular e propagar erros que ocorrem durante
* as operações de rede, como falha ao iniciar o servidor, aceitar conexões
* ou conectar a um host remoto.
* </p>
*/
public class ConnectionException extends RuntimeException {
/**
* Construtor da exceção com uma mensagem detalhada e a causa original.
*
* @param msg A mensagem detalhada.
* @param cause A exceção original que causou o erro.
*/
public ConnectionException(String msg, Throwable cause) {
super(msg, cause);
}
}
Main
/**
* Ponto de entrada da aplicação TCP Echo.
* <p>
* A classe {@code Main} é responsável por inicializar a aplicação,
* determinando se ela deve ser executada como um cliente ou um servidor
* com base nos argumentos da linha de comando.
* </p>
*/
public class Main {
/**
* @param args Argumentos da linha de comando fornecidos no início da aplicação.
*/
public static void main(String[] args) {
var parser = new FlagParser(args);
var server = new TcpEchoServerImpl();
var client = new TcpEchoClientImpl();
var starter = new ApplicationStarter(parser, client, server);
starter.execute();
}
}
