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::netdocumentation — https://docs.rs/tokio/latest/tokio/net/- "High Performance Browser Networking" — Ilya Grigorik — entendendo TCP e UDP em profundidade — https://hpbn.co
miocrate — I/O não-bloqueante de baixo nível (base do Tokio) — https://docs.rs/mioquinncrate — implementação de QUIC/HTTP3 em Rust puro — https://docs.rs/quinn