Neste projeto vamos construir um servidor de chat em tempo real usando WebSockets em Rust. Utilizaremos tokio como runtime assíncrono e tokio-tungstenite para a comunicação WebSocket. O servidor vai suportar múltiplos clientes conectados simultaneamente, apelidos de usuários e broadcast de mensagens para todos os participantes.
WebSockets permitem comunicação bidirecional persistente entre cliente e servidor, ideal para aplicações em tempo real como chats, jogos multiplayer e dashboards ao vivo. Rust, com seu modelo de concorrência seguro e eficiente, é uma excelente escolha para construir servidores WebSocket de alto desempenho.
O Que Vamos Construir
Um servidor de chat com as seguintes funcionalidades:
- Conexões WebSocket simultâneas de múltiplos clientes
- Definição de apelido ao conectar (primeira mensagem)
- Broadcast de mensagens para todos os clientes conectados
- Notificação quando um usuário entra ou sai do chat
- Lista de usuários online
- Mensagens formatadas com timestamp e nome do remetente
Estrutura do Projeto
chat-websocket/
├── Cargo.toml
└── src/
└── main.rs
Configurando o Projeto
cargo new chat-websocket
cd chat-websocket
Configure o Cargo.toml:
[package]
name = "chat-websocket"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
tokio-tungstenite = "0.24"
futures-util = "0.3"
chrono = "0.4"
Usamos tokio-tungstenite para WebSockets, futures-util para trabalhar com streams e sinks, e chrono para timestamps nas mensagens.
Passo 1: Definindo o Estado Compartilhado
O estado do chat precisa ser compartilhado entre todas as conexões ativas. Vamos usar canais de broadcast para distribuir mensagens e um Mutex para manter a lista de usuários:
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::sync::broadcast;
// Mensagem enviada pelo sistema de broadcast
#[derive(Debug, Clone)]
pub struct MensagemChat {
pub remetente: String,
pub conteudo: String,
pub timestamp: String,
}
impl MensagemChat {
fn formatada(&self) -> String {
format!("[{}] {}: {}", self.timestamp, self.remetente, self.conteudo)
}
}
// Estado compartilhado entre todas as conexões
pub struct EstadoChat {
// Canal de broadcast para mensagens
pub transmissor: broadcast::Sender<MensagemChat>,
// Mapa de usuários conectados: endereço -> apelido
pub usuarios: Mutex<HashMap<String, String>>,
}
impl EstadoChat {
fn novo() -> Arc<Self> {
let (transmissor, _) = broadcast::channel(1000);
Arc::new(EstadoChat {
transmissor,
usuarios: Mutex::new(HashMap::new()),
})
}
fn adicionar_usuario(&self, endereco: &str, apelido: &str) {
let mut usuarios = self.usuarios.lock().unwrap();
usuarios.insert(endereco.to_string(), apelido.to_string());
}
fn remover_usuario(&self, endereco: &str) -> Option<String> {
let mut usuarios = self.usuarios.lock().unwrap();
usuarios.remove(endereco)
}
fn listar_usuarios(&self) -> Vec<String> {
let usuarios = self.usuarios.lock().unwrap();
usuarios.values().cloned().collect()
}
fn enviar_mensagem(&self, remetente: &str, conteudo: &str) {
let mensagem = MensagemChat {
remetente: remetente.to_string(),
conteudo: conteudo.to_string(),
timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
};
// Ignora erro se não há receptores
let _ = self.transmissor.send(mensagem);
}
}
Usamos broadcast::channel do tokio para enviar mensagens a todos os clientes. Cada conexão recebe um Receiver que escuta novas mensagens. O Mutex protege o mapa de usuários para acesso thread-safe.
Passo 2: Tratando Conexões WebSocket
Cada cliente WebSocket é tratado em uma task separada. A primeira mensagem define o apelido:
use futures_util::{SinkExt, StreamExt};
use tokio::net::TcpStream;
use tokio_tungstenite::tungstenite::Message;
async fn tratar_conexao(
stream: TcpStream,
endereco: String,
estado: Arc<EstadoChat>,
) {
// Aceita o handshake WebSocket
let ws_stream = match tokio_tungstenite::accept_async(stream).await {
Ok(ws) => ws,
Err(e) => {
eprintln!("Erro no handshake WebSocket de {}: {}", endereco, e);
return;
}
};
println!("Nova conexão WebSocket: {}", endereco);
let (mut escritor, mut leitor) = ws_stream.split();
// Solicita apelido ao cliente
let _ = escritor
.send(Message::Text(
"Bem-vindo ao chat! Digite seu apelido:".to_string(),
))
.await;
// Aguarda a primeira mensagem como apelido
let apelido = match leitor.next().await {
Some(Ok(msg)) => {
let texto = msg.to_text().unwrap_or("Anônimo").trim().to_string();
if texto.is_empty() {
"Anônimo".to_string()
} else {
texto
}
}
_ => {
eprintln!("Cliente {} desconectou antes de informar apelido", endereco);
return;
}
};
// Registra o usuário
estado.adicionar_usuario(&endereco, &apelido);
// Notifica todos que o usuário entrou
estado.enviar_mensagem("Sistema", &format!("{} entrou no chat", apelido));
// Envia lista de usuários online
let usuarios = estado.listar_usuarios();
let _ = escritor
.send(Message::Text(format!(
"Usuários online: {}",
usuarios.join(", ")
)))
.await;
// Cria um receptor de broadcast para esta conexão
let mut receptor = estado.transmissor.subscribe();
let apelido_clone = apelido.clone();
let endereco_clone = endereco.clone();
// Loop principal: recebe mensagens do cliente e do broadcast
loop {
tokio::select! {
// Mensagem recebida do cliente via WebSocket
msg = leitor.next() => {
match msg {
Some(Ok(Message::Text(texto))) => {
let texto = texto.trim().to_string();
if texto == "/usuarios" {
let usuarios = estado.listar_usuarios();
let _ = escritor
.send(Message::Text(format!(
"Usuários online: {}",
usuarios.join(", ")
)))
.await;
} else if texto == "/sair" {
break;
} else if !texto.is_empty() {
estado.enviar_mensagem(&apelido_clone, &texto);
}
}
Some(Ok(Message::Close(_))) | None => break,
_ => {}
}
}
// Mensagem recebida via broadcast (de outro usuário)
resultado = receptor.recv() => {
match resultado {
Ok(mensagem) => {
// Não envia a própria mensagem de volta
if mensagem.remetente != apelido_clone {
let _ = escritor
.send(Message::Text(mensagem.formatada()))
.await;
}
}
Err(_) => break,
}
}
}
}
// Limpa ao desconectar
if let Some(apelido) = estado.remover_usuario(&endereco_clone) {
estado.enviar_mensagem("Sistema", &format!("{} saiu do chat", apelido));
}
println!("Conexão encerrada: {}", endereco_clone);
}
O tokio::select! permite aguardar simultaneamente mensagens do cliente (via WebSocket) e mensagens de broadcast (de outros clientes). Comandos especiais como /usuarios e /sair são tratados antes de transmitir a mensagem.
Passo 3: Iniciando o Servidor TCP
O servidor escuta conexões TCP e promove cada uma para WebSocket:
use tokio::net::TcpListener;
async fn iniciar_servidor() {
let estado = EstadoChat::novo();
let endereco = "0.0.0.0:8080";
let listener = TcpListener::bind(endereco).await.unwrap();
println!("Servidor de chat WebSocket rodando em ws://{}", endereco);
println!("Conecte-se com um cliente WebSocket para começar a conversar.");
loop {
match listener.accept().await {
Ok((stream, addr)) => {
let estado_clone = estado.clone();
let endereco = addr.to_string();
tokio::spawn(async move {
tratar_conexao(stream, endereco, estado_clone).await;
});
}
Err(e) => {
eprintln!("Erro ao aceitar conexão: {}", e);
}
}
}
}
Cada conexão aceita gera uma nova task com tokio::spawn, permitindo que o servidor trate milhares de conexões simultâneas sem bloquear.
Passo 4: Montando o main.rs Completo
Aqui está o código completo do src/main.rs:
use futures_util::{SinkExt, StreamExt};
use std::collections::HashMap;
use std::sync::{Arc, Mutex};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast;
use tokio_tungstenite::tungstenite::Message;
// === Modelos ===
#[derive(Debug, Clone)]
pub struct MensagemChat {
pub remetente: String,
pub conteudo: String,
pub timestamp: String,
}
impl MensagemChat {
fn formatada(&self) -> String {
format!("[{}] {}: {}", self.timestamp, self.remetente, self.conteudo)
}
}
pub struct EstadoChat {
pub transmissor: broadcast::Sender<MensagemChat>,
pub usuarios: Mutex<HashMap<String, String>>,
}
impl EstadoChat {
fn novo() -> Arc<Self> {
let (transmissor, _) = broadcast::channel(1000);
Arc::new(EstadoChat {
transmissor,
usuarios: Mutex::new(HashMap::new()),
})
}
fn adicionar_usuario(&self, endereco: &str, apelido: &str) {
let mut usuarios = self.usuarios.lock().unwrap();
usuarios.insert(endereco.to_string(), apelido.to_string());
}
fn remover_usuario(&self, endereco: &str) -> Option<String> {
let mut usuarios = self.usuarios.lock().unwrap();
usuarios.remove(endereco)
}
fn listar_usuarios(&self) -> Vec<String> {
let usuarios = self.usuarios.lock().unwrap();
usuarios.values().cloned().collect()
}
fn enviar_mensagem(&self, remetente: &str, conteudo: &str) {
let mensagem = MensagemChat {
remetente: remetente.to_string(),
conteudo: conteudo.to_string(),
timestamp: chrono::Local::now().format("%H:%M:%S").to_string(),
};
let _ = self.transmissor.send(mensagem);
}
}
// === Tratamento de conexão ===
async fn tratar_conexao(stream: TcpStream, endereco: String, estado: Arc<EstadoChat>) {
let ws_stream = match tokio_tungstenite::accept_async(stream).await {
Ok(ws) => ws,
Err(e) => {
eprintln!("Erro no handshake WebSocket de {}: {}", endereco, e);
return;
}
};
println!("Nova conexão WebSocket: {}", endereco);
let (mut escritor, mut leitor) = ws_stream.split();
let _ = escritor
.send(Message::Text(
"Bem-vindo ao chat! Digite seu apelido:".to_string(),
))
.await;
let apelido = match leitor.next().await {
Some(Ok(msg)) => {
let texto = msg.to_text().unwrap_or("Anônimo").trim().to_string();
if texto.is_empty() {
"Anônimo".to_string()
} else {
texto
}
}
_ => return,
};
estado.adicionar_usuario(&endereco, &apelido);
estado.enviar_mensagem("Sistema", &format!("{} entrou no chat", apelido));
let usuarios = estado.listar_usuarios();
let _ = escritor
.send(Message::Text(format!(
"Usuários online: {}",
usuarios.join(", ")
)))
.await;
let mut receptor = estado.transmissor.subscribe();
let apelido_clone = apelido.clone();
let endereco_clone = endereco.clone();
loop {
tokio::select! {
msg = leitor.next() => {
match msg {
Some(Ok(Message::Text(texto))) => {
let texto = texto.trim().to_string();
if texto == "/usuarios" {
let usuarios = estado.listar_usuarios();
let _ = escritor
.send(Message::Text(format!(
"Usuários online: {}",
usuarios.join(", ")
)))
.await;
} else if texto == "/sair" {
break;
} else if !texto.is_empty() {
estado.enviar_mensagem(&apelido_clone, &texto);
}
}
Some(Ok(Message::Close(_))) | None => break,
_ => {}
}
}
resultado = receptor.recv() => {
match resultado {
Ok(mensagem) => {
if mensagem.remetente != apelido_clone {
let _ = escritor
.send(Message::Text(mensagem.formatada()))
.await;
}
}
Err(_) => break,
}
}
}
}
if let Some(apelido) = estado.remover_usuario(&endereco_clone) {
estado.enviar_mensagem("Sistema", &format!("{} saiu do chat", apelido));
}
println!("Conexão encerrada: {}", endereco_clone);
}
// === Main ===
#[tokio::main]
async fn main() {
let estado = EstadoChat::novo();
let endereco = "0.0.0.0:8080";
let listener = TcpListener::bind(endereco).await.unwrap();
println!("Servidor de chat WebSocket rodando em ws://{}", endereco);
println!("Conecte-se com um cliente WebSocket para começar a conversar.");
println!("Comandos: /usuarios (lista online), /sair (desconectar)");
loop {
match listener.accept().await {
Ok((stream, addr)) => {
let estado_clone = estado.clone();
let endereco = addr.to_string();
tokio::spawn(async move {
tratar_conexao(stream, endereco, estado_clone).await;
});
}
Err(e) => eprintln!("Erro ao aceitar conexão: {}", e),
}
}
}
Como Executar
Compile e inicie o servidor:
cargo run
Para testar, você pode usar websocat (instale com cargo install websocat):
# Terminal 1 - Primeiro cliente
websocat ws://localhost:8080
# Digite seu apelido: Maria
# Olá pessoal!
# Terminal 2 - Segundo cliente
websocat ws://localhost:8080
# Digite seu apelido: João
# [14:30:05] Maria: Olá pessoal!
# Oi Maria!
# Comandos disponíveis:
# /usuarios - Lista quem está online
# /sair - Desconecta do chat
Você também pode testar no navegador usando JavaScript:
const ws = new WebSocket('ws://localhost:8080');
ws.onmessage = (e) => console.log(e.data);
ws.send('MeuApelido'); // Define o apelido
ws.send('Olá do navegador!'); // Envia mensagem
Desafios para Expandir
Salas de chat — Implemente suporte a múltiplas salas. Usuários podem criar salas com
/criar sala_nomee trocar de sala com/entrar sala_nome, recebendo apenas mensagens da sala atual.Mensagens privadas — Adicione o comando
/msg apelido mensagempara enviar mensagens diretas que somente o destinatário recebe.Histórico de mensagens — Armazene as últimas 50 mensagens de cada sala e envie o histórico quando um novo cliente se conectar, para que ele veja o contexto da conversa.
Interface web — Crie uma página HTML com CSS e JavaScript que o servidor sirva em
GET /, proporcionando uma interface gráfica para o chat direto no navegador.Persistência e autenticação — Adicione registro de usuários com senha, persistindo contas em um arquivo JSON ou banco SQLite, exigindo login antes de entrar no chat.
Veja Também
- Channels na Biblioteca Padrão — Conceitos de canais de comunicação entre threads
- Mutex para Exclusão Mútua — Proteção do estado compartilhado
- Arc para Compartilhamento — Como Arc funciona para compartilhar dados
- Tokio: Guia Completo — Tudo sobre o runtime assíncrono Tokio
- Async/Await em Profundidade — Entenda programação assíncrona em Rust