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);import asyncio
TAMANHO_MAXIMO = 1024
async def atender(leitor: asyncio.StreamReader, escritor: asyncio.StreamWriter):
try:
while True:
# readline acumula os bytes até a quebra de linha
linha = await asyncio.wait_for(leitor.readline(), timeout=30)
if not linha:
break # o cliente fechou a conexão
mensagem = linha.decode().rstrip("\n")
escritor.write(f"OK {mensagem.upper()}\n".encode())
await escritor.drain()
except (asyncio.TimeoutError, asyncio.LimitOverrunError, ValueError):
pass # ociosa demais ou linha longa demais
finally:
escritor.close()
async def main():
# limit: tamanho máximo de uma linha no buffer de leitura
servidor = await asyncio.start_server(atender, port=9000, limit=TAMANHO_MAXIMO)
async with servidor:
await servidor.serve_forever()
asyncio.run(main())package main
import (
"bufio"
"fmt"
"log"
"net"
"strings"
"time"
)
func atender(conexao net.Conn) {
defer conexao.Close()
leitor := bufio.NewScanner(conexao)
leitor.Buffer(make([]byte, 1024), 1024) // tamanho máximo de uma linha
for {
conexao.SetReadDeadline(time.Now().Add(30 * time.Second))
if !leitor.Scan() {
return // fim da conexão, ociosidade ou linha longa demais
}
mensagem := leitor.Text()
fmt.Fprintf(conexao, "OK %s\n", strings.ToUpper(mensagem))
}
}
func main() {
ouvinte, err := net.Listen("tcp", ":9000")
if err != nil {
log.Fatal(err)
}
for {
conexao, err := ouvinte.Accept()
if err != nil {
continue
}
go atender(conexao) // uma goroutine por conexão
}
}import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.net.ServerSocket;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
public class ServidorDeLinhas {
public static void main(String[] args) throws IOException {
try (var servidor = new ServerSocket(9000)) {
while (true) {
Socket conexao = servidor.accept();
Thread.startVirtualThread(() -> atender(conexao)); // uma thread virtual por conexão
}
}
}
static void atender(Socket conexao) {
try (conexao;
var leitor = new BufferedReader(
new InputStreamReader(conexao.getInputStream(), StandardCharsets.UTF_8));
var escritor = new PrintWriter(conexao.getOutputStream(), true, StandardCharsets.UTF_8)) {
conexao.setSoTimeout(30_000); // tempo limite de leitura
String mensagem;
// readLine acumula os bytes até a quebra de linha
while ((mensagem = leitor.readLine()) != null) {
if (mensagem.length() > 1024) {
escritor.println("ERRO mensagem longa demais");
return;
}
escritor.println("OK " + mensagem.toUpperCase());
}
} catch (IOException e) {
// ociosa demais ou conexão interrompida
}
}
}using System.Net;
using System.Net.Sockets;
using System.Text;
var ouvinte = new TcpListener(IPAddress.Any, 9000);
ouvinte.Start();
while (true)
{
var conexao = await ouvinte.AcceptTcpClientAsync();
_ = Atender(conexao); // uma tarefa por conexão
}
static async Task Atender(TcpClient conexao)
{
using (conexao)
{
var fluxo = conexao.GetStream();
using var leitor = new StreamReader(fluxo, Encoding.UTF8);
using var escritor = new StreamWriter(fluxo, Encoding.UTF8) { AutoFlush = true };
try
{
while (true)
{
using var tempo = new CancellationTokenSource(TimeSpan.FromSeconds(30));
// ReadLineAsync acumula os bytes até a quebra de linha
var mensagem = await leitor.ReadLineAsync(tempo.Token);
if (mensagem is null) break; // o cliente fechou
if (mensagem.Length > 1024)
{
await escritor.WriteLineAsync("ERRO mensagem longa demais");
break;
}
await escritor.WriteLineAsync($"OK {mensagem.ToUpperInvariant()}");
}
}
catch (Exception e) when (e is OperationCanceledException or IOException)
{
// ociosa demais ou conexão interrompida
}
}
}O que muda de uma linguagem para outra
| Leitura de linhas | Limite de tamanho da linha | Modelo de concorrência | |
|---|---|---|---|
| Node | readline sobre o soquete | Conferido depois de montar a linha | Laço de eventos |
| Python | StreamReader.readline | limit no buffer, aplicado durante a leitura | asyncio |
| Go | bufio.Scanner | Buffer, aplicado durante a leitura | Uma goroutine por conexão |
| Java | BufferedReader.readLine | Conferido depois de montar a linha | Uma thread virtual por conexão |
| C# | StreamReader.ReadLineAsync | Conferido depois de montar a linha | Uma 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 9000O 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
| Ferramenta | Para que serve | Exemplo |
|---|---|---|
ss | Listar conexões e portas abertas | ss -tnp mostra as conexões TCP com o processo dono |
curl -w | Medir cada etapa de um pedido HTTP | Tempo de DNS, conexão, TLS e primeiro byte |
tcpdump | Capturar os pacotes de verdade | tcpdump -i any port 5432 mostra o tráfego com o banco |
tc netem | Simular latência e perda para testar | tc 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 -lUm 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.