Програма широкомовного чату
У цій вправі ми хочемо використати наші нові знання для реалізації програми чату. У нас є чат-сервер, до якого підключаються клієнти і публікують свої повідомлення. Клієнт читає повідомлення користувача зі стандартного вводу і надсилає їх на сервер. Сервер чату транслює кожне повідомлення, яке він отримує, усім клієнтам.
Для цього ми використовуємо трансляційний канал на сервері та tokio_websockets
для зв’язку між клієнтом і сервером.
Створіть новий проект Cargo та додайте такі залежності:
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.40.0", features = ["full"] }
tokio-websockets = { version = "0.9.0", features = ["client", "fastrand", "server", "sha1_smol"] }
Необхідні API
Вам знадобляться такі функції з tokio
і tokio_websockets
. Витратьте кілька хвилин на ознайомлення з API.
- StreamExt::next(), реалізований
WebsocketStream
: для асинхронного читання повідомлень з потоку Websocket. - SinkExt::send(), реалізований
WebsocketStream
: для асинхронного надсилання повідомлень у потоці Websocket. - Lines::next_line(): для асинхронного читання повідомлень користувача зі стандартного вводу.
- Sender::subscribe(): для підписки на канал трансляції.
Два бінарні файли
Зазвичай у проекті Cargo можна мати лише один бінарний файл і один файл rc/main.rs
. У цьому проекті нам потрібні два бінарних файли. Один для клієнта і один для сервера. Потенційно ви могли б зробити їх двома окремими проектами Cargo, але ми збираємося помістити їх в один проект Cargo з двома бінарними файлами. Для того, щоб це працювало, клієнтський і серверний код має знаходитися у каталозі src/bin
(дивиться документацію).
Скопіюйте наступний серверний та клієнтський код у файли src/bin/server.rs
та src/bin/client.rs
відповідно. Ваше завдання - доповнити ці файли, як описано нижче.
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: Підказку дивіться в описі завдання нижче. } #[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!("слухаємо на порту 2000"); loop { let (socket, addr) = listener.accept().await?; println!("Нове з'єднання з {addr:?}"); let bcast_tx = bcast_tx.clone(); tokio::spawn(async move { // Обернути необроблений TCP потік у веб-сокет. 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: Підказку дивіться в описі завдання нижче. }
Запуск бінарних файлів
Запустіть сервер за допомогою:
cargo run --bin server
і клієнт за допомогою:
cargo run --bin client
Завдання
- Реалізуйте функцію
handle_connection
уsrc/bin/server.rs
.- Підказка: використовуйте
tokio::select!
для одночасного виконання двох завдань у безперервному циклі. Одне завдання отримує повідомлення від клієнта і транслює їх. Інше надсилає повідомлення, отримані сервером, клієнту.
- Підказка: використовуйте
- Завершіть основну функцію в
src/bin/client.rs
.- Підказка: як і раніше, використовуйте
tokio::select!
у безперервному циклі для одночасного виконання двох завдань: (1) читання повідомлень користувача зі стандартного вводу та надсилання їх на сервер, і (2) отримання повідомлень від сервера, і відображення їх для користувача.
- Підказка: як і раніше, використовуйте
- Необов’язково: коли ви закінчите, змініть код, щоб транслювати повідомлення всім клієнтам, крім відправника повідомлення.