Aplicación de chat de difusión
En este ejercicio, queremos usar nuestros nuevos conocimientos para implementar una aplicación de chat de difusión. Disponemos de un servidor de chat al que los clientes se conectan y publican sus mensajes. El cliente lee los mensajes de usuario de la entrada estándar y los envía al servidor. El servidor del chat transmite cada mensaje que recibe a todos los clientes.
For this, we use a broadcast channel on the server, and tokio_websockets
for the communication between the client and the server.
Crea un proyecto de Cargo y añade las siguientes dependencias:
Cargo.toml
:
[package]
name = "chat-async"
version = "0.1.0"
edition = "2021"
[dependencies]
futures-util = { version = "0.3.28", features = ["sink"] }
http = "0.2.9"
tokio = { version = "1.28.1", features = ["full"] }
tokio-websockets = { version = "0.4.0", features = ["client", "fastrand", "server", "sha1_smol"] }
Las APIs necesarias
You are going to need the following functions from tokio
and tokio_websockets
. Spend a few minutes to familiarize yourself with the API.
- StreamExt::next() implemented by
WebsocketStream
: for asynchronously reading messages from a Websocket Stream. - SinkExt::send() implementado por
WebsocketStream
: permite enviar mensajes de forma asíncrona a través de un flujo WebSocket. - Lines::next_line(): para la lectura asíncrona de mensajes de usuario de la entrada estándar.
- Sender::subscribe(): para suscribirse a un canal en abierto.
Dos binarios
Normalmente, en un proyecto de Cargo, solo puedes tener un archivo binario y un archivo src/main.rs
. En este proyecto, se necesitan dos binarios, uno para el cliente y otro para el servidor. Puedes convertirlos en dos proyectos de Cargo independientes, pero los incluiremos en un solo proyecto de Cargo con dos binarios. Para que funcione, el código del cliente y del servidor deben aparecer en src/bin
(consulta la documentación).
Copia el fragmento de código del servidor y del cliente que aparecen más abajo en src/bin/server.rs
y src/bin/client.rs
, respectivamente. Tu tarea es completar estos archivos como se describe a continuación.
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: For a hint, see the description of the task below. } #[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!("listening on port 2000"); loop { let (socket, addr) = listener.accept().await?; println!("New connection from {addr:?}"); let bcast_tx = bcast_tx.clone(); tokio::spawn(async move { // Wrap the raw TCP stream into a 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: For a hint, see the description of the task below. }
Ejecutar los binarios
Ejecuta el servidor con:
cargo run --bin server
y el cliente con:
cargo run --bin client
Tasks
- Implementa la función
handle_connection
ensrc/bin/server.rs
.- Sugerencia: usa
tokio::select!
para realizar dos tareas simultáneamente en un bucle continuo. Una tarea recibe mensajes del cliente y los transmite. La otra envía los mensajes que recibe el servidor al cliente.
- Sugerencia: usa
- Completa la función principal en
src/bin/client.rs
.- Sugerencia: al igual que antes, usa
tokio::select!
en un bucle continuo para realizar dos tareas simultáneamente: (1) leer los mensajes del usuario desde la entrada estándar y enviarlos al servidor, y (2) recibir mensajes del servidor y mostrárselos al usuario.
- Sugerencia: al igual que antes, usa
- Opcional: cuando termines, cambia el código para difundir mensajes a todos los clientes, excepto al remitente.