5.2 广播聊天应用程序

02-广播聊天应用程序 — Comprehensive Rust

译文 · 基于 Comprehensive Rust

原文链接: https://google.github.io/comprehensive-rust/concurrency/async-exercises/chat-app.html

5.2 广播聊天应用程序

在本练习中,我们希望使用新知识来实现广播聊天 应用程序。我们有一个聊天服务器,客户端可以连接到该服务器并发布他们的信息 消息。客户端从标准输入读取用户消息,并将其发送 到服务器。聊天服务器将其收到的每条消息广播给所有人 客户。

为此,我们在服务器上使用广播通道,并且 tokio_websockets用于客户端和服务器之间的通信。

创建一个新的 Cargo 项目并添加以下依赖项:

Cargo.toml:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
[package]
name = "chat-async"
version = "0.1.0"
edition = "2024"

[dependencies]
futures-util = { version = "0.3.32", features = ["sink"] }
http = "1.4.1"
tokio = { version = "1.52.3", features = ["full"] }
tokio-websockets = { version = "0.13.2", features = ["client", "fastrand", "server", "sha1_smol"] }

所需的 API

您将需要以下功能tokio和 tokio_websockets。花一些时间熟悉 API。

两个二进制文件

通常在 Cargo 项目中,您只能拥有一个二进制文件,并且一个src/main.rs文件。在这个项目中,我们需要两个二进制文件。一份给客户,一份给客户 服务器。您可以将它们设为两个独立的 Cargo 项目,但我们 将它们放入具有两个二进制文件的单个 Cargo 项目中。为了做到这一点, 客户端和服务器代码应该放在下面src/bin(参见 文档)。

将以下服务器和客户端代码复制到src/bin/server.rs和src/bin/client.rs, 分别。您的任务是完成这些文件 如下所述。

src/bin/server.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// Copyright 2024 Google LLC
// SPDX-License-Identifier: Apache-2.0
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::{Sender, channel};
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!("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 {
            // 将原始 TCP 流包装到 websocket 中。
            let (_req, ws_stream) = ServerBuilder::new().accept(socket).await?;

            handle_connection(addr, ws_stream, bcast_tx).await
        });
    }
}

src/bin/client.rs:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
// Copyright 2024 Google LLC
// SPDX-License-Identifier: Apache-2.0
use futures_util::SinkExt;
use futures_util::stream::StreamExt;
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:有关提示,请参阅下面的任务描述。

}

运行二进制文件

使用以下命令运行服务器:

1
cargo run --bin server

和客户:

1
cargo run --bin client

任务

  • 实施handle_connection函数于src/bin/server.rs.
    • 提示:使用tokio::select!用于同时执行两个任务 连续循环。一个任务接收来自客户端的消息并广播 他们。另一个将服务器收到的消息发送给客户端。
  • 完成主要功能src/bin/client.rs.
    • 提示:和以前一样,使用tokio::select!在连续循环中同时进行 执行两个任务:(1)从标准输入读取用户消息和 将它们发送到服务器,以及 (2) 从服务器接收消息,以及 为用户显示它们。
  • 可选:完成后,更改代码以向所有人广播消息 客户端,而是消息的发送者。
最后修改 August 11, 2026: 更新 (70a5af133)