پخش برنامه چت

در این تمرین، ما می‌خواهیم از دانش جدید خود برای پیاده سازی یک برنامه broadcast chat استفاده کنیم. ما یک سرور چت داریم که کاربران به آن متصل می‌شوند و پیام‌های خود را منتشر می‌کنند. کلاینت پیام‌های کاربر را از ورودی استاندارد می‌خواند و آنها را به سرور ارسال می‌کند. سرور چت هر پیامی را که دریافت می‌کند برای همه کاربران پخش می‌کند.

برای این کار، از broadcast channel در سمت سرور و 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: برای خواندن ناهمزمان پیام‌ها از یک جریان وب سوکت.
  • SinkExt::send() پیاده‌سازی شده توسط WebSocketStream: برای ارسال ناهمزمان پیام‌ها در یک Websocket Stream.
  • Lines::next_line(): برای خواندن ناهمزمان پیام‌های کاربر از ورودی استاندارد.
  • Sender::subscribe(): برای اشتراک در یک broadcast channel.

دو باینری

به طور معمول در یک پروژه Cargo، شما می توانید فقط یک فایل باینری و یک فایل src/main.rs داشته باشید. در این پروژه به دو باینری نیاز داریم. یکی برای کلاینت و دیگری برای سرور. شما به طور بالقوه می‌توانید آنها را در دو پروژه Cargo جداگانه بسازید، اما ما آنها را در یک پروژه Cargo واحد با دو باینری قرار می‌دهیم. برای این کار، کلاینت و کد سرور باید زیر src/bin قرار گیرند (بهdocumentation مراجعه کنید ).

کد سرور و کلاینت زیر را به ترتیب در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: 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!("اتصال جدید از {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.

}

راه‌اندازی باینری

سرور را راه‌اندازی کنید با استفاده از:

cargo run --bin server

و این کلاینت با:

cargo run --bin client

Task

  • تابع handle_connection را در src/bin/server.rs پیاده‌سازی کنید.
    • نکته: از tokio::select! برای انجام همزمان دو task در یک حلقه پیوسته استفاده کنید. یک task پیام‌هایی را از کلاینت دریافت می‌کند و آنها را پخش(broadcast) می‌کند. دیگری پیام‌های دریافت شده توسط سرور را برای کاربر ارسال می‌کند.
  • تابع اصلی را در src/bin/client.rs تکمیل کنید.
    • نکته: مانند قبل، از tokio::select! در یک حلقه پیوسته برای انجام همزمان دو task استفاده کنید: (۱) خواندن پیام های کاربر از ورودی استاندارد و ارسال آنها به سرور و (۲) دریافت پیام از سرور و نمایش آنها برای کاربر.
  • اختیاری: پس از اتمام کار، کد را تغییر دهید تا پیام‌ها برای همه کلاینت‌ها، به جز فرستنده پیام، منتشر شود.