---
title: "Chat em Tempo Real com WebSocket"
url: "https://rustlang.com.br/projetos/chat-websocket/"
markdown_url: "https://rustlang.com.br/projetos/chat-websocket.MD"
description: "Construa um servidor de chat em tempo real em Rust usando tokio e tokio-tungstenite com salas, apelidos e broadcast de mensagens."
date: "2026-02-24"
author: "Equipe Rust Brasil"
---

# Chat em Tempo Real com WebSocket

Construa um servidor de chat em tempo real em Rust usando tokio e tokio-tungstenite com salas, apelidos e broadcast de mensagens.


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

```bash
cargo new chat-websocket
cd chat-websocket
```

Configure o `Cargo.toml`:

```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:

```rust
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:

```rust
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:

```rust
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`:

```rust
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:

```bash
cargo run
```

Para testar, você pode usar `websocat` (instale com `cargo install websocat`):

```bash
# 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:

```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

1. **Salas de chat** — Implemente suporte a múltiplas salas. Usuários podem criar salas com `/criar sala_nome` e trocar de sala com `/entrar sala_nome`, recebendo apenas mensagens da sala atual.

2. **Mensagens privadas** — Adicione o comando `/msg apelido mensagem` para enviar mensagens diretas que somente o destinatário recebe.

3. **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.

4. **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.

5. **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](/stdlib/channels/) — Conceitos de canais de comunicação entre threads
- [Mutex para Exclusão Mútua](/stdlib/mutex/) — Proteção do estado compartilhado
- [Arc para Compartilhamento](/stdlib/rc-arc/) — Como Arc funciona para compartilhar dados
- [Tokio: Guia Completo](/artigos/tokio-guia-completo/) — Tudo sobre o runtime assíncrono Tokio
- [Async/Await em Profundidade](/artigos/async-await-profundidade/) — Entenda programação assíncrona em Rust
