Rust

Programação de Redes — TCP, UDP e Sockets Já leu

15 min de leitura

Programação de Redes — TCP, UDP e Sockets
Toda a infraestrutura digital moderna roda sobre protocolos de rede. Quando você usa Axum ou reqwest, está usando abstrações sobre TCP. Entender as camadas abaixo dessas abstrações não é apenas academicamente interessant

 

Toda a infraestrutura digital moderna roda sobre protocolos de rede. Quando você usa Axum ou reqwest, está usando abstrações sobre TCP. Entender as camadas abaixo dessas abstrações não é apenas academicamente interessante — é essencial quando você precisa otimizar performance, implementar protocolos customizados, ou debugar problemas que as abstrações escondem.

Neste artigo descemos até os sockets: TCP para comunicação confiável e orientada a conexão, UDP para comunicação rápida sem garantias, e a infraestrutura assíncrona que Tokio oferece para lidar com milhares de conexões simultâneas sem o custo de uma thread por conexão.


TCP — Fundamentos

TCP (Transmission Control Protocol) garante entrega ordenada e sem erros. É o protocolo sob HTTP, HTTPS, SSH, e a maioria dos protocolos de aplicação. Em Rust, std::net::TcpListener e TcpStream são as primitivas síncronas:

use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::thread;

fn tratar_cliente(stream: TcpStream) {
    let endereco = stream.peer_addr()
        .map(|a| a.to_string())
        .unwrap_or_else(|_| "desconhecido".to_string());

    println!("[{endereco}] Conectado");

    let mut leitor = BufReader::new(stream.try_clone().unwrap());
    let mut escritor = stream;

    let mut linha = String::new();
    loop {
        linha.clear();
        match leitor.read_line(&mut linha) {
            Ok(0) => {
                println!("[{endereco}] Desconectado");
                break;
            }
            Ok(_) => {
                let mensagem = linha.trim();
                println!("[{endereco}] Recebido: {mensagem}");

                if mensagem.eq_ignore_ascii_case("sair") {
                    let _ = writeln!(escritor, "Até logo!");
                    break;
                }

                // Ecoa a mensagem em maiúsculas
                let resposta = format!("{}
", mensagem.to_uppercase());
                if escritor.write_all(resposta.as_bytes()).is_err() {
                    break;
                }
            }
            Err(e) => {
                eprintln!("[{endereco}] Erro: {e}");
                break;
            }
        }
    }
}

fn servidor_eco() -> std::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878")?;
    println!("Servidor eco rodando em 127.0.0.1:7878");
    println!("Conecte com: telnet 127.0.0.1 7878");

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                // Uma thread por cliente — simples mas não escalável
                thread::spawn(|| tratar_cliente(stream));
            }
            Err(e) => eprintln!("Erro ao aceitar conexão: {e}"),
        }
    }

    Ok(())
}

Cliente TCP

use std::io::{BufRead, BufReader, Write};
use std::net::TcpStream;

fn cliente_eco(mensagens: &[&str]) -> std::io::Result<()> {
    let stream = TcpStream::connect("127.0.0.1:7878")?;
    println!("Conectado ao servidor");

    let mut leitor = BufReader::new(stream.try_clone()?);
    let mut escritor = stream;

    for &msg in mensagens {
        // Envia mensagem
        writeln!(escritor, "{msg}")?;
        println!("Enviado: {msg}");

        // Aguarda resposta
        let mut resposta = String::new();
        leitor.read_line(&mut resposta)?;
        println!("Resposta: {}", resposta.trim());
    }

    writeln!(escritor, "sair")?;
    let mut despedida = String::new();
    leitor.read_line(&mut despedida)?;
    println!("{}", despedida.trim());

    Ok(())
}

TCP Assíncrono com Tokio

O modelo síncrono com uma thread por cliente não escala bem. Com Tokio, podemos tratar milhares de conexões com um pool pequeno de threads:

use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;

async fn tratar_cliente_async(
    stream: TcpStream,
    mut rx_broadcast: broadcast::Receiver<String>,
) {
    let endereco = stream.peer_addr()
        .map(|a| a.to_string())
        .unwrap_or_else(|_| "?".to_string());

    println!("[{endereco}] Conectado");

    let (leitor, mut escritor) = stream.into_split();
    let mut leitor = BufReader::new(leitor);
    let mut linha = String::new();

    loop {
        tokio::select! {
            // Lê mensagem do cliente
            resultado = leitor.read_line(&mut linha) => {
                match resultado {
                    Ok(0) => {
                        println!("[{endereco}] Desconectado");
                        break;
                    }
                    Ok(_) => {
                        let msg = linha.trim().to_string();
                        println!("[{endereco}] → {msg}");
                        linha.clear();
                    }
                    Err(e) => {
                        eprintln!("[{endereco}] Erro de leitura: {e}");
                        break;
                    }
                }
            }

            // Recebe mensagem do broadcast (chat)
            resultado = rx_broadcast.recv() => {
                match resultado {
                    Ok(msg) => {
                        if escritor.write_all(
                            format!("{msg}
").as_bytes()
                        ).await.is_err() {
                            break;
                        }
                    }
                    Err(_) => break,
                }
            }
        }
    }
}

#[tokio::main]
async fn main() -> tokio::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:7878").await?;
    let (tx, _rx) = broadcast::channel::<String>(100);

    println!("Servidor de chat em 127.0.0.1:7878");

    loop {
        let (stream, addr) = listener.accept().await?;
        println!("Nova conexão: {addr}");

        let rx = tx.subscribe();
        tokio::spawn(tratar_cliente_async(stream, rx));
    }
}

Implementando um protocolo customizado

Protocolos reais raramente são baseados em texto linha a linha. Vamos implementar um protocolo binário simples com framing:

// Protocolo: [4 bytes: tamanho] [N bytes: payload]

use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream};
use serde::{Deserialize, Serialize};

#[derive(Debug, Serialize, Deserialize)]
enum Mensagem {
    Ping { id: u32 },
    Pong { id: u32, latencia_ms: u64 },
    Dados { chave: String, valor: Vec<u8> },
    Erro { codigo: u16, descricao: String },
}

async fn escrever_mensagem(
    stream: &mut TcpStream,
    msg: &Mensagem,
) -> tokio::io::Result<()> {
    let payload = serde_json::to_vec(msg)
        .map_err(|e| tokio::io::Error::new(
            tokio::io::ErrorKind::InvalidData,
            e
        ))?;

    // Envia tamanho (4 bytes big-endian) + payload
    let tamanho = payload.len() as u32;
    stream.write_all(&tamanho.to_be_bytes()).await?;
    stream.write_all(&payload).await?;
    stream.flush().await?;
    Ok(())
}

async fn ler_mensagem(stream: &mut TcpStream) -> tokio::io::Result<Mensagem> {
    // Lê os 4 bytes do tamanho
    let mut buf_tamanho = [0u8; 4];
    stream.read_exact(&mut buf_tamanho).await?;
    let tamanho = u32::from_be_bytes(buf_tamanho) as usize;

    // Limita o tamanho para evitar ataques
    const MAX_TAMANHO: usize = 1024 * 1024; // 1MB
    if tamanho > MAX_TAMANHO {
        return Err(tokio::io::Error::new(
            tokio::io::ErrorKind::InvalidData,
            format!("Mensagem muito grande: {tamanho} bytes")
        ));
    }

    // Lê o payload
    let mut payload = vec![0u8; tamanho];
    stream.read_exact(&mut payload).await?;

    serde_json::from_slice(&payload)
        .map_err(|e| tokio::io::Error::new(
            tokio::io::ErrorKind::InvalidData,
            e
        ))
}

async fn servidor_protocolo() -> tokio::io::Result<()> {
    let listener = TcpListener::bind("127.0.0.1:9000").await?;
    println!("Servidor de protocolo em 127.0.0.1:9000");

    loop {
        let (mut stream, addr) = listener.accept().await?;
        println!("Conexão de: {addr}");

        tokio::spawn(async move {
            loop {
                match ler_mensagem(&mut stream).await {
                    Ok(msg) => {
                        println!("Recebido: {:?}", msg);

                        let resposta = match msg {
                            Mensagem::Ping { id } => {
                                Mensagem::Pong { id, latencia_ms: 1 }
                            }
                            Mensagem::Dados { chave, .. } => {
                                Mensagem::Dados {
                                    chave: format!("eco:{chave}"),
                                    valor: b"ok".to_vec(),
                                }
                            }
                            _ => Mensagem::Erro {
                                codigo: 400,
                                descricao: "Mensagem não suportada".to_string(),
                            }
                        };

                        if escrever_mensagem(&mut stream, &resposta)
                            .await.is_err()
                        {
                            break;
                        }
                    }
                    Err(e) => {
                        eprintln!("Erro: {e}");
                        break;
                    }
                }
            }
        });
    }
}

UDP — Comunicação sem conexão

UDP (User Datagram Protocol) não garante entrega, ordem ou ausência de duplicatas. Em troca, é mais rápido e sem overhead de handshake. Ideal para streaming de vídeo, jogos online, DNS, e telemetria.

use std::net::UdpSocket;

fn servidor_udp() -> std::io::Result<()> {
    let socket = UdpSocket::bind("127.0.0.1:8080")?;
    println!("Servidor UDP em 127.0.0.1:8080");

    let mut buf = [0u8; 65535]; // tamanho máximo de um datagrama UDP

    loop {
        let (n, origem) = socket.recv_from(&mut buf)?;
        let mensagem = String::from_utf8_lossy(&buf[..n]);
        println!("[{origem}] {mensagem}");

        // Envia resposta
        let resposta = format!("ECO: {mensagem}");
        socket.send_to(resposta.as_bytes(), origem)?;
    }
}

fn cliente_udp(mensagens: &[&str]) -> std::io::Result<()> {
    // bind em porta 0 = SO escolhe porta disponível
    let socket = UdpSocket::bind("0.0.0.0:0")?;
    socket.connect("127.0.0.1:8080")?;

    let mut buf = [0u8; 65535];

    for &msg in mensagens {
        socket.send(msg.as_bytes())?;
        println!("Enviado: {msg}");

        let n = socket.recv(&mut buf)?;
        println!("Resposta: {}", String::from_utf8_lossy(&buf[..n]));
    }

    Ok(())
}

UDP Assíncrono com Tokio

use tokio::net::UdpSocket;
use std::sync::Arc;

// Servidor de métricas UDP — recebe dados de múltiplos clientes
#[tokio::main]
async fn servidor_metricas() -> tokio::io::Result<()> {
    let socket = Arc::new(UdpSocket::bind("0.0.0.0:9125").await?);
    println!("Servidor de métricas UDP em :9125 (formato StatsD)");

    let mut buf = vec![0u8; 65535];

    loop {
        let (n, origem) = socket.recv_from(&mut buf).await?;
        let dados = String::from_utf8_lossy(&buf[..n]).to_string();

        // Processa em task separada para não bloquear o recebimento
        tokio::spawn(async move {
            processar_metrica(&origem.to_string(), &dados);
        });
    }
}

fn processar_metrica(origem: &str, dados: &str) {
    // Formato StatsD simplificado: nome:valor|tipo
    // Ex: "requisicoes:1|c" (contador)
    //     "latencia:42|ms" (tempo)
    //     "memoria:1024|g" (gauge)

    let partes: Vec<&str> = dados.split('|').collect();
    if partes.len() != 2 {
        return;
    }

    let kv: Vec<&str> = partes[0].split(':').collect();
    if kv.len() != 2 {
        return;
    }

    let (nome, valor_str, tipo) = (kv[0], kv[1], partes[1]);

    if let Ok(valor) = valor_str.parse::<f64>() {
        let tipo_desc = match tipo {
            "c"  => "contador",
            "ms" => "tempo (ms)",
            "g"  => "gauge",
            _    => "desconhecido",
        };
        println!("[{origem}] {nome} = {valor} ({tipo_desc})");
    }
}

Pool de conexões TCP

Em servidores de alta performance, criar e destruir conexões TCP é caro. Um pool de conexões reutiliza conexões existentes:

use tokio::net::TcpStream;
use tokio::sync::Mutex;
use std::sync::Arc;
use std::collections::VecDeque;
use std::time::{Duration, Instant};

struct ConexaoPoolada {
    stream: TcpStream,
    criada_em: Instant,
}

struct PoolConexoes {
    endereco: String,
    disponiveis: Mutex<VecDeque<ConexaoPoolada>>,
    max_conexoes: usize,
    max_idade: Duration,
}

impl PoolConexoes {
    fn novo(endereco: &str, max: usize) -> Arc<Self> {
        Arc::new(PoolConexoes {
            endereco: endereco.to_string(),
            disponiveis: Mutex::new(VecDeque::new()),
            max_conexoes: max,
            max_idade: Duration::from_secs(30),
        })
    }

    async fn obter(&self) -> tokio::io::Result<TcpStream> {
        let mut disponiveis = self.disponiveis.lock().await;

        // Remove conexões expiradas
        disponiveis.retain(|c| {
            c.criada_em.elapsed() < self.max_idade
        });

        // Reutiliza conexão existente
        if let Some(conn) = disponiveis.pop_front() {
            println!("Reutilizando conexão do pool");
            return Ok(conn.stream);
        }

        // Cria nova conexão
        println!("Criando nova conexão para {}", self.endereco);
        TcpStream::connect(&self.endereco).await
    }

    async fn devolver(&self, stream: TcpStream) {
        let mut disponiveis = self.disponiveis.lock().await;

        if disponiveis.len() < self.max_conexoes {
            disponiveis.push_back(ConexaoPoolada {
                stream,
                criada_em: Instant::now(),
            });
            println!("Conexão devolvida ao pool ({})", disponiveis.len());
        }
        // Se pool cheio, a conexão é descartada (drop fecha o socket)
    }

    async fn tamanho(&self) -> usize {
        self.disponiveis.lock().await.len()
    }
}

Um programa completo: servidor de chat com salas

Vamos construir um servidor de chat completo com salas, usando TCP assíncrono:

use std::collections::HashMap;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Mutex};

type SalasMap = Arc<Mutex<HashMap<String, broadcast::Sender<String>>>>;

struct Servidor {
    salas: SalasMap,
}

impl Servidor {
    fn novo() -> Arc<Self> {
        Arc::new(Servidor {
            salas: Arc::new(Mutex::new(HashMap::new())),
        })
    }

    async fn obter_ou_criar_sala(
        &self,
        nome: &str,
    ) -> broadcast::Sender<String> {
        let mut salas = self.salas.lock().await;
        salas
            .entry(nome.to_string())
            .or_insert_with(|| {
                let (tx, _) = broadcast::channel(100);
                println!("Nova sala criada: {nome}");
                tx
            })
            .clone()
    }

    async fn listar_salas(&self) -> Vec<String> {
        let salas = self.salas.lock().await;
        let mut nomes: Vec<String> = salas.keys().cloned().collect();
        nomes.sort();
        nomes
    }
}

async fn tratar_conexao(
    stream: TcpStream,
    servidor: Arc<Servidor>,
) {
    let addr = stream.peer_addr()
        .map(|a| a.to_string())
        .unwrap_or_else(|_| "?".to_string());

    let (leitor, mut escritor) = stream.into_split();
    let mut leitor = BufReader::new(leitor);
    let mut linha = String::new();
    let mut apelido = format!("Anon@{addr}");
    let mut sala_atual: Option<broadcast::Sender<String>> = None;
    let mut sala_rx: Option<broadcast::Receiver<String>> = None;
    let mut nome_sala = String::new();

    let boas_vindas = "\
══════════════════════════════════
\
  Bem-vindo ao Chat Rust!
\
  Comandos:
\
    /nome <apelido>   — define seu nome
\
    /sala <nome>      — entra em uma sala
\
    /sair             — sai da sala atual
\
    /salas            — lista salas ativas
\
    /quit             — desconecta
\
══════════════════════════════════
";

    let _ = escritor.write_all(boas_vindas.as_bytes()).await;

    loop {
        linha.clear();

        let resultado = if let Some(ref mut rx) = sala_rx {
            // Com sala: aguarda mensagem do cliente OU da sala
            tokio::select! {
                r = leitor.read_line(&mut linha) => {
                    match r {
                        Ok(0) | Err(_) => break,
                        Ok(_) => None,
                    }
                }
                msg = rx.recv() => {
                    match msg {
                        Ok(m) => Some(m),
                        Err(_) => None,
                    }
                }
            }
        } else {
            // Sem sala: apenas lê do cliente
            match leitor.read_line(&mut linha).await {
                Ok(0) | Err(_) => break,
                Ok(_) => None,
            }
        };

        // Mensagem da sala para o cliente
        if let Some(msg_sala) = resultado {
            let _ = escritor.write_all(
                format!("{msg_sala}
").as_bytes()
            ).await;
            continue;
        }

        let cmd = linha.trim().to_string();
        if cmd.is_empty() {
            continue;
        }

        if cmd == "/quit" {
            let _ = escritor.write_all(b"Tchau!
").await;
            break;
        }

        if cmd.starts_with("/nome ") {
            apelido = cmd[6..].trim().to_string();
            let msg = format!("Nome definido como: {apelido}
");
            let _ = escritor.write_all(msg.as_bytes()).await;
            continue;
        }

        if cmd == "/salas" {
            let salas = servidor.listar_salas().await;
            let msg = if salas.is_empty() {
                "Nenhuma sala ativa.
".to_string()
            } else {
                format!("Salas: {}
", salas.join(", "))
            };
            let _ = escritor.write_all(msg.as_bytes()).await;
            continue;
        }

        if cmd == "/sair" {
            if let Some(ref tx) = sala_atual {
                let aviso = format!("*** {apelido} saiu da sala ***");
                let _ = tx.send(aviso);
            }
            sala_atual = None;
            sala_rx = None;
            nome_sala.clear();
            let _ = escritor.write_all(b"Saiu da sala.
").await;
            continue;
        }

        if cmd.starts_with("/sala ") {
            let nova_sala = cmd[6..].trim().to_string();

            // Sai da sala atual
            if let Some(ref tx) = sala_atual {
                let aviso = format!("*** {apelido} saiu ***");
                let _ = tx.send(aviso);
            }

            let tx = servidor.obter_ou_criar_sala(&nova_sala).await;
            let rx = tx.subscribe();

            let aviso = format!("*** {apelido} entrou na sala {nova_sala} ***");
            let _ = tx.send(aviso);

            sala_atual = Some(tx);
            sala_rx = Some(rx);
            nome_sala = nova_sala.clone();

            let msg = format!("Entrou na sala: {nova_sala}
");
            let _ = escritor.write_all(msg.as_bytes()).await;
            continue;
        }

        // Mensagem normal — envia para a sala
        if let Some(ref tx) = sala_atual {
            let msg = format!("[{nome_sala}] {apelido}: {cmd}");
            println!("{msg}");
            let _ = tx.send(msg);
        } else {
            let _ = escritor.write_all(
                b"Entre em uma sala com /sala <nome>
"
            ).await;
        }
    }

    // Cleanup ao desconectar
    if let Some(ref tx) = sala_atual {
        let aviso = format!("*** {apelido} desconectou ***");
        let _ = tx.send(aviso);
    }
    println!("[{addr}] Desconectado");
}

#[tokio::main]
async fn main() -> tokio::io::Result<()> {
    let servidor = Servidor::novo();
    let listener = TcpListener::bind("0.0.0.0:6667").await?;

    println!("Servidor de chat em :6667");
    println!("Conecte com: telnet localhost 6667");
    println!("Ou: nc localhost 6667");

    loop {
        let (stream, addr) = listener.accept().await?;
        println!("Nova conexão: {addr}");

        let servidor_clone = Arc::clone(&servidor);
        tokio::spawn(tratar_conexao(stream, servidor_clone));
    }
}

Teste com múltiplos terminais:

# Terminal 1
nc localhost 6667
/nome Alice
/sala rust
Olá a todos!

# Terminal 2
nc localhost 6667
/nome Bob
/sala rust
Oi Alice!

Configurações de socket avançadas

use std::net::{TcpListener, TcpStream};
use std::time::Duration;

fn configurar_socket(stream: &TcpStream) -> std::io::Result<()> {
    // Timeout de leitura e escrita
    stream.set_read_timeout(Some(Duration::from_secs(30)))?;
    stream.set_write_timeout(Some(Duration::from_secs(10)))?;

    // TCP_NODELAY — desabilita o algoritmo de Nagle
    // Importante para protocolos de baixa latência
    stream.set_nodelay(true)?;

    // SO_KEEPALIVE — detecta conexões mortas
    stream.set_keepalive(Some(Duration::from_secs(60)))?;

    // TTL do pacote IP
    stream.set_ttl(64)?;

    println!("Nodelay:  {}", stream.nodelay()?);
    println!("TTL:      {}", stream.ttl()?);

    Ok(())
}

fn configurar_listener(listener: &TcpListener) -> std::io::Result<()> {
    // SO_REUSEADDR — permite reiniciar servidor sem esperar TIME_WAIT
    // Já habilitado por padrão no Rust std

    println!("Endereço: {}", listener.local_addr()?);

    Ok(())
}

Resolvendo nomes DNS

use std::net::ToSocketAddrs;
use tokio::net::lookup_host;

fn resolver_sincrono(host: &str) -> std::io::Result<()> {
    let enderecos: Vec<_> = format!("{host}:80")
        .to_socket_addrs()?
        .collect();

    println!("Endereços de {host}:");
    for addr in &enderecos {
        println!("  {addr}");
    }

    Ok(())
}

async fn resolver_assincrono(host: &str) -> tokio::io::Result<()> {
    let enderecos: Vec<_> = lookup_host(format!("{host}:443"))
        .await?
        .collect();

    println!("Endereços assíncronos de {host}:");
    for addr in &enderecos {
        println!("  {addr}");
    }

    Ok(())
}

Métricas de rede: o que monitorar

Em produção, um servidor TCP deve expor métricas para monitoramento. As mais importantes:

Conexões ativas — pico pode indicar DDoS ou vazamento de conexões.

Latência de handshake — tempo do accept() ao primeiro byte. Alta latência indica congestionamento ou problema de DNS.

Throughput — bytes por segundo. Quedas repentinas indicam problemas de rede.

Erros de leitura/escrita — conexões resetadas pelo cliente são normais; muitas indicam problema.

Fila de backlog — conexões aguardando accept(). Se encher, novas conexões são recusadas. Configure com TcpListener::bind e set_backlog quando disponível.


Fontes e leituras recomendadas

  • Rust Standard Library — std::net — https://doc.rust-lang.org/std/net/
  • Tokio — Network tutorial — https://tokio.rs/tokio/tutorial/io
  • "Beej's Guide to Network Programming" — guia clássico de sockets em C, os conceitos se aplicam diretamente — https://beej.us/guide/bgnet/
  • tokio::net documentation — https://docs.rs/tokio/latest/tokio/net/
  • "High Performance Browser Networking" — Ilya Grigorik — entendendo TCP e UDP em profundidade — https://hpbn.co
  • mio crate — I/O não-bloqueante de baixo nível (base do Tokio) — https://docs.rs/mio
  • quinn crate — implementação de QUIC/HTTP3 em Rust puro — https://docs.rs/quinn
Comentários

Mais em Rust

Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade
Contribuindo com o Ecossistema Rust — Open Source, RFCs e a Comunidade

Rust — Artigo #49 Contribuindo com o Ecossistema Rust — Open Source, RFCs e a...

Rust para Data Science — Processamento de Dados com Polars e ndarray
Rust para Data Science — Processamento de Dados com Polars e ndarray

Rust — Artigo #41 Rust para Data Science — Processamento de Dados com Polars...

Structs — Criando seus próprios tipos de dados
Structs — Criando seus próprios tipos de dados

&nbsp; At&eacute; agora trabalhamos com tipos primitivos &mdash; inteiros, s...