Програма широкомовного чату
У цій вправі ми хочемо використати наші нові знання для реалізації програми чату. У нас є чат-сервер, до якого підключаються клієнти і публікують свої повідомлення. Клієнт читає повідомлення користувача зі стандартного вводу і надсилає їх на сервер. Сервер чату транслює кожне повідомлення, яке він отримує, усім клієнтам.
Для цього ми використовуємо трансляційний канал на сервері та tokio_websockets
для зв’язку між клієнтом і сервером.
Створіть новий проект Cargo та додайте такі залежності:
Cargo.toml:
[package]
name = "chat-async"
version = "0.1.0"
edition = "2021"
[dependencies]
futures-util = { version = "0.3.31", features = ["sink"] }
http = "1.1.0"
tokio = { version = "1.41.1", features = ["full"] }
tokio-websockets = { version = "0.10.1", 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) отримання повідомлень від сервера, і відображення їх для користувача.
- Підказка: як і раніше, використовуйте
- Необов’язково: коли ви закінчите, змініть код, щоб транслювати повідомлення всім клієнтам, крім відправника повідомлення.