第九章 Rust 实战小项目:CLI 聊天室#
"The best way to learn is by doing." — Richard Branson
前八章我们学习了 Rust 的核心概念:所有权与借用、结构体与枚举、模式匹配、Trait 与泛型、错误处理、异步编程。现在是时候把它们融会贯通了。
本章我们将从零构建一个终端聊天室——一个 TCP 服务器和客户端,支持多人同时在线聊天。这个项目虽然不大,但几乎涉及了前八章的所有知识点。
mindmap
root((CLI 聊天室))
需求
多人聊天
昵称
在线列表
服务端
TCP listener
session 管理
广播
客户端
输入循环
消息接收
优雅退出
练习价值
所有权
错误处理
async
9.1 项目规划#
9.1.1 功能需求#
功能 |
描述 |
|---|---|
多人聊天 |
多个客户端同时连接,消息广播给所有人 |
用户昵称 |
连接时设置昵称,消息带昵称前缀 |
系统通知 |
用户加入/离开时通知所有人 |
优雅退出 |
客户端输入 |
在线列表 |
客户端输入 |
9.1.2 技术选型#
组件 |
选择 |
理由 |
|---|---|---|
异步运行时 |
Tokio |
成熟、高性能、生态丰富 |
网络协议 |
TCP |
简单可靠,适合入门 |
消息格式 |
行分隔的文本 |
简单直观 |
广播机制 |
|
多生产者多消费者 |
9.1.3 项目结构#
chat-room/
├── Cargo.toml
└── src/
├── main.rs # 入口:根据参数启动 server 或 client
├── server.rs # 聊天服务器
├── client.rs # 聊天客户端
└── protocol.rs # 消息协议定义
9.1.4 知识点覆盖#
graph TD
P[CLI 聊天室项目] --> O[所有权与借用<br>ch03-04]
P --> S[结构体与枚举<br>ch05]
P --> T[Trait 与泛型<br>ch06]
P --> E[错误处理<br>ch07]
P --> A[异步编程<br>ch08]
O --> O1["move 闭包<br>Arc 共享"]
S --> S1["Message 枚举<br>ChatRoom 结构体"]
T --> T1["Display trait<br>From trait"]
E --> E1["Result + ?<br>anyhow"]
A --> A1["async/await<br>tokio::spawn<br>broadcast channel"]
style P fill:#f9f,stroke:#333
9.2 消息协议#
首先定义客户端和服务器之间的消息格式。
9.2.1 protocol.rs#
// src/protocol.rs
use std::fmt;
/// 聊天室中的消息类型
#[derive(Debug, Clone)]
pub enum Message {
/// 用户加入聊天室
Join { username: String },
/// 用户离开聊天室
Leave { username: String },
/// 普通聊天消息
Chat { username: String, content: String },
/// 系统消息
System(String),
/// 在线用户列表
UserList(Vec<String>),
}
impl Message {
/// 将消息序列化为一行文本(用于网络传输)
pub fn encode(&self) -> String {
match self {
Message::Join { username } => format!("/join {}\n", username),
Message::Leave { username } => format!("/leave {}\n", username),
Message::Chat { username, content } => format!("/chat {} {}\n", username, content),
Message::System(text) => format!("/system {}\n", text),
Message::UserList(users) => format!("/users {}\n", users.join(",")),
}
}
/// 从一行文本解析消息
pub fn decode(line: &str) -> Result<Self, ProtocolError> {
let line = line.trim();
if let Some(rest) = line.strip_prefix("/join ") {
Ok(Message::Join {
username: rest.to_string(),
})
} else if let Some(rest) = line.strip_prefix("/leave ") {
Ok(Message::Leave {
username: rest.to_string(),
})
} else if let Some(rest) = line.strip_prefix("/chat ") {
// 格式:/chat username content...
let (username, content) = rest
.split_once(' ')
.ok_or(ProtocolError::MalformedMessage)?;
Ok(Message::Chat {
username: username.to_string(),
content: content.to_string(),
})
} else if let Some(rest) = line.strip_prefix("/system ") {
Ok(Message::System(rest.to_string()))
} else if let Some(rest) = line.strip_prefix("/users ") {
let users: Vec<String> = rest.split(',').map(|s| s.to_string()).collect();
Ok(Message::UserList(users))
} else {
Err(ProtocolError::UnknownCommand)
}
}
}
/// 实现 Display trait —— 用于终端显示(ch06 知识)
impl fmt::Display for Message {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Message::Join { username } => {
write!(f, "📥 {} joined the chat", username)
}
Message::Leave { username } => {
write!(f, "📤 {} left the chat", username)
}
Message::Chat { username, content } => {
write!(f, "💬 {}: {}", username, content)
}
Message::System(text) => {
write!(f, "⚙️ {}", text)
}
Message::UserList(users) => {
write!(f, "👥 Online ({}): {}", users.len(), users.join(", "))
}
}
}
}
/// 协议错误类型(ch07 知识:自定义错误)
#[derive(Debug)]
pub enum ProtocolError {
MalformedMessage,
UnknownCommand,
}
impl fmt::Display for ProtocolError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ProtocolError::MalformedMessage => write!(f, "Malformed message"),
ProtocolError::UnknownCommand => write!(f, "Unknown command"),
}
}
}
impl std::error::Error for ProtocolError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_encode_decode_chat() {
let msg = Message::Chat {
username: "Walter".to_string(),
content: "Hello everyone!".to_string(),
};
let encoded = msg.encode();
let decoded = Message::decode(&encoded).unwrap();
match decoded {
Message::Chat { username, content } => {
assert_eq!(username, "Walter");
assert_eq!(content, "Hello everyone!");
}
_ => panic!("Expected Chat message"),
}
}
#[test]
fn test_encode_decode_join() {
let msg = Message::Join {
username: "Alice".to_string(),
};
let encoded = msg.encode();
let decoded = Message::decode(&encoded).unwrap();
match decoded {
Message::Join { username } => assert_eq!(username, "Alice"),
_ => panic!("Expected Join message"),
}
}
#[test]
fn test_display() {
let msg = Message::Chat {
username: "Bob".to_string(),
content: "Hi!".to_string(),
};
assert_eq!(format!("{}", msg), "💬 Bob: Hi!");
}
}
知识点回顾:
枚举 + 关联数据(ch05):
Message枚举的每个变体携带不同数据模式匹配(ch05):
match解构枚举Display trait(ch06):为终端友好输出实现
Display自定义错误(ch07):
ProtocolError枚举单元测试:
#[cfg(test)]模块
9.3 聊天服务器#
9.3.1 server.rs#
// src/server.rs
use crate::protocol::Message;
use anyhow::{Context, Result};
use std::collections::HashMap;
use std::sync::Arc;
use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::{TcpListener, TcpStream};
use tokio::sync::{broadcast, Mutex};
/// 聊天室状态(ch05 知识:结构体)
struct ChatRoom {
/// 在线用户:username -> () (用 HashMap 快速查找)
users: HashMap<String, ()>,
}
impl ChatRoom {
fn new() -> Self {
Self {
users: HashMap::new(),
}
}
fn add_user(&mut self, username: &str) -> bool {
if self.users.contains_key(username) {
false // 用户名已存在
} else {
self.users.insert(username.to_string(), ());
true
}
}
fn remove_user(&mut self, username: &str) {
self.users.remove(username);
}
fn list_users(&self) -> Vec<String> {
self.users.keys().cloned().collect()
}
fn user_count(&self) -> usize {
self.users.len()
}
}
/// 启动聊天服务器
pub async fn run(addr: &str) -> Result<()> {
let listener = TcpListener::bind(addr)
.await
.context(format!("Failed to bind to {}", addr))?;
println!("🚀 Chat server listening on {}", addr);
// 广播 channel:所有客户端共享(ch08 知识:broadcast channel)
let (tx, _rx) = broadcast::channel::<Message>(100);
// 聊天室状态:Arc + Mutex 多任务共享(ch08 知识)
let room = Arc::new(Mutex::new(ChatRoom::new()));
loop {
let (socket, peer_addr) = listener.accept().await?;
println!("📡 New connection from {}", peer_addr);
// 为每个连接克隆共享资源(ch03 知识:Arc::clone 不是深拷贝)
let tx = tx.clone();
let rx = tx.subscribe();
let room = Arc::clone(&room);
// 为每个连接创建一个异步任务(ch08 知识:tokio::spawn)
tokio::spawn(async move {
if let Err(e) = handle_client(socket, tx, rx, room).await {
eprintln!("❌ Error handling {}: {}", peer_addr, e);
}
});
}
}
/// 处理单个客户端连接
async fn handle_client(
socket: TcpStream,
tx: broadcast::Sender<Message>,
mut rx: broadcast::Receiver<Message>,
room: Arc<Mutex<ChatRoom>>,
) -> Result<()> {
let (reader, mut writer) = socket.into_split();
let mut reader = BufReader::new(reader);
let mut line = String::new();
// === 第一步:获取用户昵称 ===
writer
.write_all(b"Welcome to Hive Chat! Enter your username: ")
.await?;
line.clear();
let n = reader.read_line(&mut line).await?;
if n == 0 {
return Ok(()); // 客户端断开
}
let username = line.trim().to_string();
if username.is_empty() {
writer.write_all(b"Username cannot be empty. Goodbye!\n").await?;
return Ok(());
}
// 注册用户到聊天室
{
let mut room = room.lock().await; // tokio::sync::Mutex(ch08 知识)
if !room.add_user(&username) {
writer
.write_all(format!("Username '{}' is taken. Goodbye!\n", username).as_bytes())
.await?;
return Ok(());
}
let count = room.user_count();
println!("✅ {} joined ({} users online)", username, count);
}
// 广播加入消息
let join_msg = Message::Join {
username: username.clone(),
};
let _ = tx.send(join_msg);
// 发送欢迎消息给当前用户
let welcome = Message::System(format!(
"Welcome, {}! Type /list for online users, /quit to exit.",
username
));
writer.write_all(welcome.encode().as_bytes()).await?;
// === 第二步:主循环 — 同时处理读和写 ===
let username_for_read = username.clone();
let username_for_write = username.clone();
let room_for_cleanup = Arc::clone(&room);
let tx_for_read = tx.clone();
// 读取任务:从客户端读消息 → 广播
let mut read_task = tokio::spawn(async move {
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line).await {
Ok(0) => break, // 连接关闭
Ok(_) => {
let trimmed = line.trim();
if trimmed.is_empty() {
continue;
}
// 处理命令
match trimmed {
"/quit" => break,
"/list" => {
let room = room.lock().await;
let users = room.list_users();
let msg = Message::UserList(users);
let _ = tx_for_read.send(msg);
}
_ => {
let msg = Message::Chat {
username: username_for_read.clone(),
content: trimmed.to_string(),
};
let _ = tx_for_read.send(msg);
}
}
}
Err(e) => {
eprintln!("Read error: {}", e);
break;
}
}
}
});
// 写入任务:从广播接收消息 → 发送给客户端
let mut write_task = tokio::spawn(async move {
loop {
match rx.recv().await {
Ok(msg) => {
// 不把自己的聊天消息回显给自己
let should_skip = matches!(
&msg,
Message::Chat { username, .. } if username == &username_for_write
);
if !should_skip {
let text = format!("{}\n", msg); // 使用 Display trait
if writer.write_all(text.as_bytes()).await.is_err() {
break;
}
}
}
Err(broadcast::error::RecvError::Lagged(n)) => {
eprintln!("Warning: {} missed {} messages", username_for_write, n);
}
Err(broadcast::error::RecvError::Closed) => break,
}
}
});
// 等待任一任务结束(ch08 知识:tokio::select!)
tokio::select! {
_ = &mut read_task => {
write_task.abort();
}
_ = &mut write_task => {
read_task.abort();
}
}
// === 第三步:清理 ===
{
let mut room = room_for_cleanup.lock().await;
room.remove_user(&username);
let count = room.user_count();
println!("👋 {} left ({} users online)", username, count);
}
let leave_msg = Message::Leave {
username: username.clone(),
};
let _ = tx.send(leave_msg);
Ok(())
}
知识点回顾:
Arc + Mutex(ch08):
ChatRoom被多个异步任务共享broadcast channel(ch08):消息广播给所有客户端
tokio::spawn(ch08):每个连接一个异步任务
tokio::select!(ch08):同时等待读和写
所有权与 move(ch03-04):
username.clone()给不同的任务错误处理(ch07):
?操作符 +anyhow
9.4 聊天客户端#
9.4.1 client.rs#
// src/client.rs
use crate::protocol::Message;
use anyhow::{Context, Result};
use tokio::io::{self, AsyncBufReadExt, AsyncWriteExt, BufReader};
use tokio::net::TcpStream;
/// 启动聊天客户端
pub async fn run(addr: &str) -> Result<()> {
let stream = TcpStream::connect(addr)
.await
.context(format!("Failed to connect to {}", addr))?;
println!("🔗 Connected to {}", addr);
let (reader, mut writer) = stream.into_split();
let mut server_reader = BufReader::new(reader);
let stdin = io::stdin();
let mut stdin_reader = BufReader::new(stdin);
// 读取服务器的欢迎消息(要求输入用户名)
let mut welcome = String::new();
server_reader.read_line(&mut welcome).await?;
print!("{}", welcome);
// 读取用户输入的用户名并发送
let mut username = String::new();
stdin_reader.read_line(&mut username).await?;
writer.write_all(username.as_bytes()).await?;
let username = username.trim().to_string();
println!("✅ Logged in as '{}'. Start chatting!\n", username);
// 从服务器读取消息 → 显示在终端
let mut read_task = tokio::spawn(async move {
let mut line = String::new();
loop {
line.clear();
match server_reader.read_line(&mut line).await {
Ok(0) => {
println!("\n🔌 Server disconnected.");
break;
}
Ok(_) => {
let trimmed = line.trim();
// 尝试解析为 Message 以获得漂亮的显示
match Message::decode(trimmed) {
Ok(msg) => println!("{}", msg), // Display trait
Err(_) => print!("{}", line), // 原样显示
}
}
Err(e) => {
eprintln!("Read error: {}", e);
break;
}
}
}
});
// 从终端读取用户输入 → 发送给服务器
let mut write_task = tokio::spawn(async move {
let mut line = String::new();
loop {
line.clear();
match stdin_reader.read_line(&mut line).await {
Ok(0) => break, // EOF (Ctrl+D)
Ok(_) => {
let trimmed = line.trim();
if trimmed == "/quit" {
writer.write_all(b"/quit\n").await.ok();
println!("👋 Goodbye!");
break;
}
// 发送原始文本,服务器会处理
if writer.write_all(line.as_bytes()).await.is_err() {
break;
}
}
Err(e) => {
eprintln!("Input error: {}", e);
break;
}
}
}
});
// 任一任务结束则退出
tokio::select! {
_ = &mut read_task => write_task.abort(),
_ = &mut write_task => read_task.abort(),
}
Ok(())
}
9.5 主入口#
9.5.1 main.rs#
// src/main.rs
mod client;
mod protocol;
mod server;
use anyhow::Result;
use std::env;
const DEFAULT_ADDR: &str = "127.0.0.1:6379";
#[tokio::main]
async fn main() -> Result<()> {
let args: Vec<String> = env::args().collect();
match args.get(1).map(|s| s.as_str()) {
Some("server") => {
let addr = args.get(2).map(|s| s.as_str()).unwrap_or(DEFAULT_ADDR);
server::run(addr).await
}
Some("client") => {
let addr = args.get(2).map(|s| s.as_str()).unwrap_or(DEFAULT_ADDR);
client::run(addr).await
}
_ => {
println!("Hive Chat - A Terminal Chat Room");
println!();
println!("Usage:");
println!(" chat-room server [addr] Start the chat server");
println!(" chat-room client [addr] Connect as a client");
println!();
println!("Default address: {}", DEFAULT_ADDR);
Ok(())
}
}
}
9.5.2 Cargo.toml#
[package]
name = "chat-room"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] }
anyhow = "1"
9.6 运行与测试#
9.6.1 启动服务器#
$ cargo run -- server
🚀 Chat server listening on 127.0.0.1:6379
9.6.2 启动客户端(开多个终端)#
终端 2:
$ cargo run -- client
🔗 Connected to 127.0.0.1:6379
Welcome to Hive Chat! Enter your username: Walter
✅ Logged in as 'Walter'. Start chatting!
Hello everyone!
终端 3:
$ cargo run -- client
🔗 Connected to 127.0.0.1:6379
Welcome to Hive Chat! Enter your username: Alice
✅ Logged in as 'Alice'. Start chatting!
📥 Alice joined the chat
💬 Walter: Hello everyone!
Hi Walter!
9.6.3 完整对话示例#
[Walter 的终端] [Alice 的终端]
────────────────────────────────── ──────────────────────────────────
Hello everyone! 📥 Alice joined the chat
📥 Alice joined the chat 💬 Walter: Hello everyone!
💬 Alice: Hi Walter! Hi Walter!
/list /list
👥 Online (2): Walter, Alice 👥 Online (2): Walter, Alice
How's the Tauri book going?
💬 Alice: Great! Almost done. Great! Almost done.
/quit 📤 Walter left the chat
👋 Goodbye!
9.7 架构回顾#
graph TB
subgraph "Server"
L[TcpListener] --> |accept| H1[handle_client Task 1]
L --> |accept| H2[handle_client Task 2]
L --> |accept| H3[handle_client Task N]
BC[broadcast::channel]
CR[ChatRoom<br>Arc+Mutex]
H1 --> |send| BC
H2 --> |send| BC
H3 --> |send| BC
BC --> |recv| H1
BC --> |recv| H2
BC --> |recv| H3
H1 --> |lock| CR
H2 --> |lock| CR
H3 --> |lock| CR
end
subgraph "Client 1"
C1R[Read Task<br>server → terminal]
C1W[Write Task<br>stdin → server]
end
subgraph "Client 2"
C2R[Read Task]
C2W[Write Task]
end
H1 <--> |TCP| C1R
H1 <--> |TCP| C1W
H2 <--> |TCP| C2R
H2 <--> |TCP| C2W
style BC fill:#fbf,stroke:#333
style CR fill:#bfb,stroke:#333
9.7.1 每个连接的生命周期#
sequenceDiagram
participant C as Client
participant S as Server (handle_client)
participant B as Broadcast Channel
participant R as ChatRoom
C->>S: TCP connect
S->>C: "Enter your username:"
C->>S: "Walter"
S->>R: add_user("Walter")
R-->>S: true (success)
S->>B: Message::Join
B-->>S: (broadcast to all)
S->>C: "Welcome, Walter!"
loop Chat
C->>S: "Hello!"
S->>B: Message::Chat
B-->>S: (broadcast to all)
Note over S: 不回显给发送者
end
C->>S: "/quit"
S->>R: remove_user("Walter")
S->>B: Message::Leave
Note over S: Task 结束
9.8 知识点总结与映射#
章节 |
知识点 |
在项目中的应用 |
|---|---|---|
ch03 所有权 |
move 语义、clone |
|
ch04 借用 |
引用、生命周期 |
|
ch05 结构体/枚举 |
枚举 + 关联数据、impl |
|
ch06 Trait |
Display、From、Error |
|
ch07 错误处理 |
Result、?、anyhow |
所有 I/O 操作的错误传播 |
ch08 异步 |
async/await、spawn、select!、channel、Arc+Mutex |
整个服务器和客户端的并发架构 |
9.9 扩展挑战#
如果你想进一步练习,可以尝试以下扩展:
挑战 1:私聊功能#
// 添加私聊命令:/msg <username> <content>
// 提示:在 Message 枚举中添加 DirectMessage 变体
// 在服务器端根据目标用户名过滤发送
挑战 2:聊天记录持久化#
// 将聊天记录保存到文件
// 提示:使用 tokio::fs::OpenOptions 异步写入
// 新用户加入时发送最近 N 条历史消息
挑战 3:JSON 协议#
// 将文本协议替换为 JSON
// 提示:使用 serde_json 序列化/反序列化 Message
// 这为后续与 Tauri 前端对接做准备
挑战 4:心跳检测#
// 定期发送心跳包,检测断线的客户端
// 提示:使用 tokio::time::interval + tokio::select!
9.10 本章小结#
在这一章中,我们从零构建了一个完整的 CLI 聊天室,综合运用了 Rust 的核心特性:
成就 |
描述 |
|---|---|
✅ 所有权实战 |
理解了 move 闭包、Arc 共享、clone 的使用场景 |
✅ 类型系统实战 |
用枚举建模消息协议,用结构体管理状态 |
✅ Trait 实战 |
为自定义类型实现 Display、Error 等标准 trait |
✅ 错误处理实战 |
在真实的 I/O 场景中使用 Result + ? + anyhow |
✅ 异步编程实战 |
tokio::spawn、broadcast channel、select! 的实际应用 |
✅ 项目组织 |
多模块项目结构、关注点分离 |
这个聊天室虽然简单,但它的架构模式——事件广播、异步任务、共享状态——正是 Tauri 桌面应用后端的核心模式。 在后续章节中,我们会把这些模式直接应用到 Tauri 的 IPC 通信、状态管理和插件系统中。
Part 1 完结! 🎉 恭喜你完成了 Rust 速成的全部内容。从下一章开始,我们将进入 Part 2——Tauri 框架深度解析,把 Rust 的力量带到桌面应用开发中。