Програма широкомовного чату

У цій вправі ми хочемо використати наші нові знання для реалізації програми чату. У нас є чат-сервер, до якого підключаються клієнти і публікують свої повідомлення. Клієнт читає повідомлення користувача зі стандартного вводу і надсилає їх на сервер. Сервер чату транслює кожне повідомлення, яке він отримує, усім клієнтам.

Для цього ми використовуємо трансляційний канал на сервері та 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.38.0", features = ["full"] }
tokio-websockets = { version = "0.8.3", 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) отримання повідомлень від сервера, і відображення їх для користувача.
  • Необов’язково: коли ви закінчите, змініть код, щоб транслювати повідомлення всім клієнтам, крім відправника повідомлення.