DevGuideTK
← Trilha Backend

TCP, UDP e IP

As camadas de transporte abaixo do HTTP, e a troca entre entrega garantida e baixa latência.

Os números desta página vêm de testes num container com latência e perda de pacotes simuladas pelo tc netem, do Linux. Eles servem para mostrar proporções; os valores absolutos dependem da rede.

Um servidor TCP que lê mensagens inteiras

O servidor recebe mensagens de texto separadas por quebra de linha e responde a cada uma. O ponto central é a leitura: em vez de tratar cada leitura do soquete como uma mensagem, o código acumula os bytes e entrega linhas completas. Todas as versões limitam o tamanho da linha e o tempo de ociosidade.

import net from "node:net";
import readline from "node:readline";

const servidor = net.createServer((conexao) => {
  conexao.setTimeout(30_000); // fecha conexões ociosas
  conexao.on("timeout", () => conexao.destroy());

  // O readline acumula os bytes e entrega uma linha completa por vez
  const linhas = readline.createInterface({ input: conexao });

  linhas.on("line", (mensagem) => {
    if (mensagem.length > 1024) {
      conexao.end("ERRO mensagem longa demais\n");
      return;
    }
    conexao.write(`OK ${mensagem.toUpperCase()}\n`);
  });

  conexao.on("error", () => conexao.destroy());
});

servidor.listen(9000);

O que muda de uma linguagem para outra

Leitura de linhasLimite de tamanho da linhaModelo de concorrência
Nodereadline sobre o soqueteConferido depois de montar a linhaLaço de eventos
PythonStreamReader.readlinelimit no buffer, aplicado durante a leituraasyncio
Gobufio.ScannerBuffer, aplicado durante a leituraUma goroutine por conexão
JavaBufferedReader.readLineConferido depois de montar a linhaUma thread virtual por conexão
C#StreamReader.ReadLineAsyncConferido depois de montar a linhaUma tarefa assíncrona por conexão

A coluna do meio esconde uma diferença importante. Em Python e em Go, o limite é aplicado enquanto a linha é lida: uma linha gigante é recusada antes de ocupar memória. Em Node, Java e C#, a conferência do exemplo acontece depois que a linha inteira foi montada, e um cliente que envie gigabytes sem quebra de linha consome memória até lá. Para um servidor exposto, leia em blocos de tamanho fixo e procure o delimitador manualmente, ou use uma biblioteca de protocolo que limite o buffer.

Testar o servidor pela linha de comando

# Conversa interativa: cada linha digitada é uma mensagem
nc localhost 9000

# Várias mensagens de uma vez, numa única escrita
printf 'um\ndois\ntres\n' | nc localhost 9000

O segundo comando envia as três mensagens juntas. Um servidor que trata cada leitura como uma mensagem responderia uma vez só, com "UM DOIS TRES" misturados. O servidor correto responde três linhas.

Ver as conexões de perto

FerramentaPara que serveExemplo
ssListar conexões e portas abertasss -tnp mostra as conexões TCP com o processo dono
curl -wMedir cada etapa de um pedido HTTPTempo de DNS, conexão, TLS e primeiro byte
tcpdumpCapturar os pacotes de verdadetcpdump -i any port 5432 mostra o tráfego com o banco
tc netemSimular latência e perda para testartc qdisc add dev eth0 root netem delay 50ms loss 1%

O curl é o jeito mais rápido de ver onde o tempo de um pedido vai:

curl -s -o /dev/null https://api.exemplo.com/pedidos \
  -w 'dns: %{time_namelookup}s\nconexão: %{time_connect}s\ntls: %{time_appconnect}s\nprimeiro byte: %{time_starttransfer}s\ntotal: %{time_total}s\n'

Se a conexão e o TLS somam a maior parte do tempo total, a aplicação provavelmente está abrindo conexões novas a cada pedido.

Conexões em TIME_WAIT

Quem fecha uma conexão TCP primeiro a mantém por um tempo no estado TIME_WAIT, para descartar pacotes atrasados. Um serviço que abre e fecha milhares de conexões por segundo acumula dezenas de milhares delas:

ss -tan state time-wait | wc -l

Um número muito alto é sintoma de falta de reaproveitamento, e no limite esgota as portas disponíveis para novas conexões de saída. A correção é reaproveitar conexões, e não mexer em parâmetros do sistema operacional.

Simular a rede para testar

A latência entre a sua máquina e o banco local é quase zero, e isso esconde problemas. O tc netem, num container com a permissão NET_ADMIN, simula a rede de produção:

docker run --rm -it --cap-add NET_ADMIN node:24-alpine sh
apk add iproute2

# 25 ms em cada sentido: 50 ms de ida e volta
tc qdisc add dev lo root netem delay 25ms

# troca por 10% de perda de pacotes
tc qdisc change dev lo root netem loss 10%

Foi com essa configuração que medimos, nesta trilha, o custo de abrir conexões novas (20 pedidos em 2.083 ms, contra 1.033 ms reaproveitando a conexão) e a diferença de entrega entre TCP e UDP com perda.

Lista de revisão

Clientes

  • Clientes HTTP, de banco e de cache são criados uma vez e compartilhados?
  • Há tempo limite de conexão, de leitura e total?
  • Conexões ociosas são validadas ou descartadas antes que o caminho as feche?
  • O número de conexões em TIME_WAIT é razoável?

Protocolos sobre TCP

  • A leitura acumula bytes até ter uma mensagem completa?
  • Existe tamanho máximo de mensagem, aplicado durante a leitura?
  • Há tempo limite de ociosidade e de recebimento da mensagem inteira?
  • HTTP, gRPC ou WebSocket não resolveriam o mesmo problema?

UDP

  • O código trata o envio como "pode não ter chegado"?
  • Há controle de ritmo, já que o UDP não tem controle de fluxo?
  • Mensagens importantes têm confirmação ou repetição feita pela aplicação?

Prompts para aprofundar

Ao abrir no ChatGPT ou no Claude, o prompt é enviado ao serviço escolhido pela URL e também fica copiado na sua área de transferência.

  • Medir o custo de uma conexão TCP

    Quero medir na prática o custo de abrir conexões TCP e o ganho de reaproveitá-las.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas:
    1. Suba comigo um container com permissão NET_ADMIN e instale o iproute2.
    2. Escreva um servidor HTTP simples e um cliente que faz 20 pedidos abrindo uma conexão nova a cada um, e outro que reaproveita a conexão.
    3. Meça sem atraso.
    4. Acrescente 50 ms de ida e volta com tc netem e meça de novo.
    5. Use o curl com -w para ver o tempo de conexão separado do resto.
    6. Repita com HTTPS e veja o custo do TLS.
    
    Em cada etapa, peça que eu preveja o resultado antes de rodar, e explique a diferença entre a previsão e o resultado.
  • Ler mensagens de um soquete corretamente

    Quero implementar a leitura correta de mensagens num servidor TCP.
    
    Pergunte qual linguagem eu uso.
    
    Vamos em etapas. Em cada uma, deixe que eu tente e depois mostre a sua versão:
    1. Um servidor que trata cada leitura como uma mensagem, e um teste que envia várias mensagens numa única escrita para mostrar o defeito.
    2. A correção com delimitador de linha.
    3. A versão com o tamanho antes dos dados, lendo exatamente o número de bytes anunciado.
    4. Limite de tamanho aplicado durante a leitura, sem montar uma mensagem gigante na memória.
    5. Tempo limite de ociosidade e de recebimento da mensagem inteira.
    6. Testes para mensagens coladas, mensagens cortadas, mensagens grandes demais e clientes lentos.
  • Investigar um problema de rede entre serviços

    Quero aprender a investigar problemas de rede entre serviços: lentidão, conexões recusadas, tempos esgotados e conexões que caem.
    
    Pergunte qual é o sintoma que eu estou vendo, se houver um.
    
    Explique, com comandos:
    1. Como usar o curl com -w para separar DNS, conexão, TLS e resposta.
    2. Como usar o ss para ver conexões abertas, estados e o processo dono, e o que significa ter muitas conexões em TIME_WAIT ou CLOSE_WAIT.
    3. Como usar o tcpdump para ver se os pacotes saem e voltam.
    4. Como diferenciar "conexão recusada", "tempo esgotado na conexão" e "tempo esgotado na leitura", e a causa provável de cada um.
    5. Como simular o problema localmente com tc netem.
    
    Termine com um roteiro de investigação na ordem em que eu deveria seguir.