项目:多人聊天服务器
这是第三阶段的毕业设计,也是本书的最后一章。在过去的二十一章中,你学习了 变量、函数、类型、枚举、模式匹配、错误处理、泛型、闭包、迭代器、Actor、并发、 元编程以及标准库。现在,是时候将所有这些技能整合起来,构建一个真实的项目: 多人聊天服务器。
聊天服务器是一个接受多个用户连接、在他们之间转发消息并管理聊天室的程序。 它是本书每个主要概念交汇处的项目。你需要类型来建模消息,枚举来表示协议, Actor 来处理并发连接,Map 来跟踪状态,错误处理来应对断线,以及标准库来执行 实际的网络操作。
我们将按以下步骤逐步构建:
- 消息类型 —— 将协议定义为枚举
- 服务器 Actor —— 管理客户端的中央协调器
- 客户端处理器 —— 每个连接的输入读取逻辑
- 聊天室 —— 支持加入和离开的多房间频道
- 主入口 —— 将所有组件组装起来
- 完整服务器 —— 组装后的完整程序
项目设置
创建一个新的 Auto 项目:
$ automan new chat_server
$ cd chat_server这是一个比之前的毕业设计更大的项目,但模式是相同的:逐步构建,每步测试, 一次添加一个功能。
设计概述
在编写任何代码之前,让我们先思考架构。聊天服务器有三个主要组件:
服务器 Actor —— 一个维护全局状态的单一 Actor:哪些用户已连接、 哪些房间存在、谁在哪个房间里。所有消息都通过这个 Actor 流转。
客户端处理器 —— 每个已连接用户一个 Actor。每个处理器从其用户读取 输入,解析命令,并将消息转发给服务器 Actor。
消息协议 —— 一个枚举,定义系统处理的每种消息类型:连接、断开、 聊天、私信、加入房间、离开房间、房间消息和系统通知。
这个设计遵循了第 15 章的 Actor 模型。服务器是唯一的协调点。客户端 处理器是独立的 Actor,通过消息与服务器通信。没有共享内存,没有锁,没有 数据竞争 —— 只有 Actor 之间发送消息。
第一步:消息类型
每个网络协议都始于对其传输消息的定义。在 Auto 中,我们将其建模为枚举。 每个变体代表聊天系统中不同种类的事件。
输出如下:
Alice connected
Alice: Hello everyone!
[PM] Alice -> Bob: Hi Bob!
[SYS] Alice has joined the lobby.
Alice disconnected让我们分析一下设计决策:
Connect(String)—— 携带用户名。当用户连接时,服务器注册他们。Disconnect(String)—— 携带用户名。服务器将他们从所有房间移除并 通知其他人。Chat(String, String)—— 携带发送者名称和消息文本。这是广播给所有 用户的公开消息。PrivateMsg(String, String, String)—— 携带发送者、接收者和文本。只有 接收者能看到此消息。System(String)—— 来自服务器本身的通知:"Alice 加入"、"Bob 离开"等。
为什么用枚举?因为编译器强制我们处理每个变体。如果稍后添加新的消息类型 (比如 Kick(String, String)),编译器会在每个未处理它的 is 表达式处 发出警告。这就是我们在第 7 章学到的穷尽模式匹配。
在 C 中,这映射为带有手动 switch 语句的标记联合。在 Python 中,映射为 带有 isinstance 检查的数据类联合。在 TypeScript 中,映射为对 kind 字段 进行 switch 的判别联合。在所有情况下,模式都是相同的:一个类型,多个变体, 穷尽处理。
第二步:服务器 Actor
服务器 Actor 是聊天系统的核心。它作为一个单一 Actor 运行(还记得[第 15 章] ch15的 Actor 吗?),维护两份状态:已连接客户端的映射和消息循环。
输出如下:
[SYS] Alice joined. Online: 1
[SYS] Bob joined. Online: 2
Alice: Hello Bob!
[PM] Bob -> Alice: Hi Alice!
[SYS] Bob left. Online: 1服务器 Actor 维护一个 clients 映射来跟踪在线用户。is 表达式中的每个 变体处理不同的消息类型:
Connect将用户添加到clients映射中并宣布其到来。Disconnect将其移除并宣布其离开。Chat打印消息(在真实服务器中,它会广播给所有客户端)。PrivateMsg打印私信标识。System打印服务器通知。
关键洞察是,所有状态变更都发生在单一 Actor 内部。clients 映射是 server 函数私有的 —— 没有其他 Actor 可以访问它。这消除了数据竞争, 不需要任何锁或互斥量。Actor 模型保证一次只处理一条消息。
在 Rust 中,服务器在带 mpsc::Receiver 的生成线程中运行。在 Python 中, 在带 queue.Queue 的守护线程中运行。在 C 中,我们用消息数组模拟消息循环。 在 TypeScript 中,同步处理数组中的消息。Auto 版本最简洁:spawn server, send server Message.Connect(...)。两行代码。
第三步:客户端处理器
每个已连接的用户都需要自己的 Actor 来读取输入并将消息转发给服务器。 这就是客户端处理器。
客户端处理器是将用户输入解析并转换为协议消息的地方。它处理以下情况:
/quit—— 发送Disconnect消息并从函数返回,这会终止该 Actor。这是 用户干净退出的方式。/msg <name> <text>—— 将命令解析为目标用户名和消息文本,然后向服务器 发送PrivateMsg。split(" ")调用(来自第 21 章的标准库导览) 将输入分成多个部分,parts[2..].join(" ")将名称之后的所有内容重新组合。纯文本 —— 任何不是命令的内容都会作为
Chat消息发送给服务器。
注意 /quit 之后的 return 语句。在 Actor 函数中,从函数返回会终止该 Actor。 这是关闭 Actor 的干净方式:处理最后一条消息,然后返回。
第四步:聊天室
真正的聊天服务器需要房间(也叫频道)。用户可以加入房间与其中所有人聊天, 完成后离开。让我们将房间管理添加到我们的消息协议和服务器中。
输出如下:
[SYS] Alice joined. Online: 1
[SYS] Bob joined. Online: 2
[SYS] Alice joined #random
[SYS] Bob joined #random
[#random] Alice: Anyone here?
(delivered to 2 members)
[#random] Bob: Yes!
(delivered to 2 members)
[SYS] Alice left #random我们添加了三个新的消息类型:
JoinRoom(String, String)—— 第一个字段是用户名,第二个是房间名。服务器 检查房间是否存在且用户不在其中,然后将其添加。LeaveRoom(String, String)—— 将用户从房间的成员列表中移除。RoomMsg(String, String, String)—— 发送到特定房间的消息。第一个字段 是房间名,第二个是发送者,第三个是文本。服务器查找房间的成员列表,将消息 发送给每个成员。
rooms 映射使用 Map<String, List<String>> —— 从房间名到用户名列表的映射。 这是嵌套集合,是我们在第 4 章和第 21 章中使用过的类型。
第五步:组装
现在让我们将各个部分组装成一个可运行的程序。主函数启动服务器 Actor 并 提供服务器管理控制台。
main 函数现在作为服务器的控制台。它启动服务器 Actor,打印横幅,然后进入 命令循环:
/quit—— 通过从main返回来关闭服务器,这会终止程序。/status—— 打印服务器的端口号。- 未知命令 —— 打印错误消息。
main 函数本身也是一个 Actor(正如我们在第 15 章学到的),所以它 使用 on input String 来接收控制台输入。这与我们在每个 Actor 中使用的模式 相同:一个带 on 的循环来接收消息。
顶部的 use std::net 导入为我们进行真正的网络操作做好了准备。在生产服务器中, main 函数会在端口上打开 TCP 监听器,并为每个传入连接生成一个 client_handler Actor。
第六步:完整服务器
以下是组装后的完整程序。这是我们的成果 —— 之前构建的所有内容汇聚在一个 简洁的 Auto 程序中。
由于完整服务器的代码与 Listing 22-06 的所有 5 种语言代码相同,此处不再重复 完整代码块。完整服务器包含以下改进:
用户名冲突检测 ——
Connect处理器检查clients.contains(name)并 拒绝重复的用户名。广播函数 ——
broadcast和broadcast_room辅助函数向所有已连接客户端 或特定房间的所有成员发送消息。欢迎消息 —— 用户连接时,服务器通过
send caller发送个人欢迎消息, 并向其他人广播其到来。私信错误处理 ——
PrivateMsg处理器在发送前检查接收者是否存在。客户端命令 ——
client_handler现在支持/join、/leave、/rooms和/msg命令。
测试服务器
测试聊天服务器有两种方式:手动测试和自动化测试。
手动测试
运行服务器并用多个终端连接:
$ automan run
=== AutoChat Server v1.0 ===
Starting on port 8080...
Server running. Press Ctrl+C to stop.在实际部署中,你会用单独的客户端进程连接。在开发阶段,代码中模拟的客户端 处理器就足够了。
自动化测试
Auto 支持 #[test] 属性进行单元测试。以下是我们会编写的测试:
#[test]
fn test_connect_increments_client_count() {
var clients: Map<String, int> = {}
clients["Alice"] = 1
assert clients.len() == 1
clients["Bob"] = 1
assert clients.len() == 2
}
#[test]
fn test_disconnect_removes_client() {
var clients: Map<String, int> = {}
clients["Alice"] = 1
clients.remove("Alice")
assert clients.len() == 0
}
#[test]
fn test_join_room_adds_member() {
var rooms: Map<String, List<String>> = { "general": [] }
rooms["general"].push("Alice".to_string())
assert rooms["general"].len() == 1
}
#[test]
fn test_leave_room_removes_member() {
var rooms: Map<String, List<String>> = { "general": [] }
rooms["general"].push("Alice".to_string())
rooms["general"].remove("Alice")
assert rooms["general"].len() == 0
}这些测试在不运行服务器的情况下验证了核心数据操作。
进阶方向
这个聊天服务器是一个坚实的基础。以下是一些扩展思路:
身份验证 —— 添加
Login(String, String)消息,在允许连接前检查凭据。 使用std::crypto存储密码哈希。持久化 —— 使用
std::fs的write_file将聊天记录保存到文件。服务器 重启时用read_file加载。WebSocket 支持 —— 使用
std::net接受 WebSocket 连接而非原始 TCP, 让浏览器可以直接连接。管理员命令 —— 添加
/kick、/ban和/create-room命令,仅限 管理员使用。消息历史 —— 为每个房间保存最近 N 条消息,新用户加入时发送给他们。
输入指示器 —— 添加
Typing(String, String)消息,向房间其他成员显示 "Alice 正在输入..."。速率限制 —— 限制用户每秒可以发送的消息数量以防止垃圾信息。
加密 —— 使用
std::crypto对私信进行端到端加密,使服务器也无法读取。
从 Hello, World! 到这里
让我们回顾一下我们走过了多远的路。
在第 1 章,你编写了第一个 Auto 程序:print("Hello, World!")。 一行打印字符串并退出的代码。那时你还不知道变量、函数或类型。
现在,在这最后一章中,你构建了一个多人聊天服务器,包含:
- 枚举(第 7 章)定义消息协议
- 模式匹配(第 7 章)处理每种消息类型
- Map 和 List(第 4 章)跟踪客户端和房间
- 字符串方法(第 21 章)解析命令
- Actor(第 15 章)用于并发服务器和客户端处理
- 标准库(第 21 章)用于
format!、split、join、contains、starts_with - 错误处理(第 12 章)用于缺失用户和无效房间
- 函数(第 2 章)将代码组织为
server、client_handler、broadcast、broadcast_room
你不仅学习了 Auto。你学习了每种编程语言的基本概念:类型、函数、集合、 控制流、并发和错误处理。Auto 的语法可以清晰地映射到 Rust、Python、C 和 TypeScript,所以你学到的所有内容都可以直接迁移。
你在本书中培养的技能 —— 设计数据结构、编写简洁的函数、显式处理错误、 推理并发问题 —— 正是专业软件工程师日常使用的技能。无论你构建 Web 服务器、 嵌入式系统、数据管道还是操作系统,这里学到的模式都将为你提供帮助。
感谢你的阅读。现在,去构建一些了不起的东西吧。
实用网络模式
除了聊天服务器,以下是三种常见的网络工具模式:解析 URL、提取查询参数和收集进程输出。
总结
这个毕业设计将第三阶段和整本书的每个概念整合在一起:
| 概念 | 使用位置 |
|---|---|
| 枚举 | Message 枚举定义协议 |
| 模式匹配 | is msg { Connect(name) => ... } |
| Map | clients: Map<String, int>、rooms: Map<String, List<String>> |
| List | 房间成员列表、消息分片 |
| 字符串方法 | split、join、starts_with、contains、to_string |
| Actor | spawn server、spawn client_handler |
| 消息传递 | send server Message.Connect(...) |
on 接收 | on msg Message { ... }、on input String { ... } |
| 错误处理 | 用户名冲突检查、缺失用户检查 |
| 标准库 | std::net、format!、println |
| 函数 | server、client_handler、broadcast、broadcast_room |
| 类型 | Map<String, List<String>>、Message 变体 |
核心要点:
先设计协议 ——
Message枚举是所有组件之间的契约。把它设计好, 其他一切都会顺畅。一个 Actor 拥有状态 —— 服务器 Actor 是客户端和房间的唯一真相来源。 这消除了整类并发 bug。
逐步构建 —— 我们从消息类型开始,然后是服务器,然后是客户端处理器, 接着是房间,最后是完整程序。每一步都可以独立测试。
Actor 模型天然可扩展 —— 添加更多功能(认证、持久化、管理命令)意味着 添加更多消息类型和更多处理程序。架构保持不变。
错误处理不是事后补丁 —— 我们检查了重复的用户名、缺失的用户和不存在 的房间。这些检查是正常控制流的一部分,而非事后添加。