Apliação de Chat de Transmissão

Neste exercício, queremos usar nosso novo conhecimento para implementar um aplicativo de bate-papo por broadcast. Temos um servidor de bate-papo ao qual os clientes se conectam e publicam suas mensagens. O cliente lê as mensagens do usuário da entrada padrão e as envia para o servidor. O servidor de bate-papo transmite cada mensagem que recebe para todos os clientes.

Para isso, usamos um canal de broadcast no servidor e tokio_websockets para a comunicação entre o cliente e o servidor.

Crie um novo projeto Cargo e adicione as seguintes dependências:

Cargo.toml:

[package]
name = "chat-async"
version = "0.1.0"
edition = "2021"

[dependencies]
futures-util = { version = "0.3.30", features = ["sink"] }
http = "1.1.0"
tokio = { version = "1.38.0", features = ["full"] }
tokio-websockets = { version = "0.8.3", features = ["client", "fastrand", "server", "sha1_smol"] }

As APIs necessárias

Você vai precisar das seguintes funções de tokio e tokio_websockets. Dedique alguns minutos para se familiarizar com a API.

  • StreamExt::next() implementado por WebSocketStream: para ler mensagens de forma assíncrona de um stream de Websocket.
  • SinkExt::send() implementado por WebSocketStream: para enviar mensagens de forma assíncrona em um stream de Websocket.
  • Lines::next_line(): para ler mensagens de forma assíncrona do usuário da entrada padrão.
  • Sender::subscribe(): para se inscrever em um canal de transmissão.

Dois binários

Normalmente em um projeto Cargo, você pode ter apenas um binário e um arquivo src/main.rs. Neste projeto, precisamos de dois binários. Um para o cliente e outro para o servidor. Você poderia potencialmente torná-los dois projetos Cargo separados, mas vamos colocá-los em um único projeto Cargo com dois binários. Para que isso funcione, o código do cliente e do servidor deve ir em src/bin (consulte a documentação).

Copie o seguinte código do servidor e do cliente para src/bin/server.rs e src/bin/client.rs, respectivamente. Sua tarefa é completar esses arquivos conforme descrito abaixo.

src/bin/server.rs:

use futures_util::sink::SinkExt;
use futures_util::stream::StreamExt;
use std::error::Error;
use std::net::SocketAddr;
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::broadcast::{channel, Sender};
use tokio_websockets::{Message, ServerBuilder, WebSocketStream};

async fn handle_connection(
    addr: SocketAddr,
    mut ws_stream: WebSocketStream<TcpStream>,
    bcast_tx: Sender<String>,
) -> Result<(), Box<dyn Error + Send + Sync>> {

    // TODO: Para uma dica, veja a descrição da tarefa abaixo.

}

#[tokio::main]
async fn main() -> Result<(), Box<dyn Error + Send + Sync>> {
    let (bcast_tx, _) = channel(16);

    let listener = TcpListener::bind("127.0.0.1:2000").await?;
    println!("ouvindo na porta 2000");

    loop {
        let (socket, addr) = listener.accept().await?;
        println!("Nova conexão de {addr:?}");
        let bcast_tx = bcast_tx.clone();
        tokio::spawn(async move {
            // Envolver o _stream_ TCP bruto em um _websocket_.
            let ws_stream = ServerBuilder::new().accept(socket).await?;

            handle_connection(addr, ws_stream, bcast_tx).await
        });
    }
}

src/bin/client.rs:

use futures_util::stream::StreamExt;
use futures_util::SinkExt;
use http::Uri;
use tokio::io::{AsyncBufReadExt, BufReader};
use tokio_websockets::{ClientBuilder, Message};

#[tokio::main]
async fn main() -> Result<(), tokio_websockets::Error> {
    let (mut ws_stream, _) =
        ClientBuilder::from_uri(Uri::from_static("ws://127.0.0.1:2000"))
            .connect()
            .await?;

    let stdin = tokio::io::stdin();
    let mut stdin = BufReader::new(stdin).lines();


    // TODO: Para uma dica, veja a descrição da tarefa abaixo.

}

Executando os binários

Execute o servidor com:

cargo run --bin server

e o cliente com:

cargo run --bin client

Tarefas

  • Implemente a função handle_connection em src/bin/server.rs.
    • Dica: Use tokio::select! para realizar concorrentemente duas tarefas em um loop contínuo. Uma tarefa recebe mensagens do cliente e as transmite. A outra envia mensagens recebidas pelo servidor para o cliente.
  • Complete a função principal em src/bin/client.rs.
    • Dica: Como antes, use tokio::select! em um loop contínuo para realizar concorrentemente duas tarefas: (1) ler mensagens do usuário da entrada padrão e enviá-las para o servidor, e (2) receber mensagens do servidor e exibi-las para o usuário.
  • Opcional: Depois de terminar, altere o código para transmitir mensagens para todos os clientes, exceto o remetente da mensagem.