Skip to content
📖 Auto 编程语言(主书)本书目录

项目:多人聊天服务器 ​

这是第三阶段的毕业设计,也是本书的最后一章。在过去的二十一章中,你学习了 变量、函数、类型、枚举、模式匹配、错误处理、泛型、闭包、迭代器、Actor、并发、 元编程以及标准库。现在,是时候将所有这些技能整合起来,构建一个真实的项目: 多人聊天服务器。

聊天服务器是一个接受多个用户连接、在他们之间转发消息并管理聊天室的程序。 它是本书每个主要概念交汇处的项目。你需要类型来建模消息,枚举来表示协议, Actor 来处理并发连接,Map 来跟踪状态,错误处理来应对断线,以及标准库来执行 实际的网络操作。

我们将按以下步骤逐步构建:

  1. 消息类型 —— 将协议定义为枚举
  2. 服务器 Actor —— 管理客户端的中央协调器
  3. 客户端处理器 —— 每个连接的输入读取逻辑
  4. 聊天室 —— 支持加入和离开的多房间频道
  5. 主入口 —— 将所有组件组装起来
  6. 完整服务器 —— 组装后的完整程序

项目设置 ​

创建一个新的 Auto 项目:

console
$ automan new chat_server
$ cd chat_server

这是一个比之前的毕业设计更大的项目,但模式是相同的:逐步构建,每步测试, 一次添加一个功能。

设计概述 ​

在编写任何代码之前,让我们先思考架构。聊天服务器有三个主要组件:

  1. 服务器 Actor —— 一个维护全局状态的单一 Actor:哪些用户已连接、 哪些房间存在、谁在哪个房间里。所有消息都通过这个 Actor 流转。

  2. 客户端处理器 —— 每个已连接用户一个 Actor。每个处理器从其用户读取 输入,解析命令,并将消息转发给服务器 Actor。

  3. 消息协议 —— 一个枚举,定义系统处理的每种消息类型:连接、断开、 聊天、私信、加入房间、离开房间、房间消息和系统通知。

这个设计遵循了第 15 章的 Actor 模型。服务器是唯一的协调点。客户端 处理器是独立的 Actor,通过消息与服务器通信。没有共享内存,没有锁,没有 数据竞争 —— 只有 Actor 之间发送消息。

第一步:消息类型 ​

每个网络协议都始于对其传输消息的定义。在 Auto 中,我们将其建模为枚举。 每个变体代表聊天系统中不同种类的事件。

聊天消息类型:带模式匹配的枚举

输出如下:

Alice connected
Alice: Hello everyone!
[PM] Alice -> Bob: Hi Bob!
[SYS] Alice has joined the lobby.
Alice disconnected

让我们分析一下设计决策:

  1. Connect(String) —— 携带用户名。当用户连接时,服务器注册他们。

  2. Disconnect(String) —— 携带用户名。服务器将他们从所有房间移除并 通知其他人。

  3. Chat(String, String) —— 携带发送者名称和消息文本。这是广播给所有 用户的公开消息。

  4. PrivateMsg(String, String, String) —— 携带发送者、接收者和文本。只有 接收者能看到此消息。

  5. System(String) —— 来自服务器本身的通知:"Alice 加入"、"Bob 离开"等。

为什么用枚举?因为编译器强制我们处理每个变体。如果稍后添加新的消息类型 (比如 Kick(String, String)),编译器会在每个未处理它的 is 表达式处 发出警告。这就是我们在第 7 章学到的穷尽模式匹配。

在 C 中,这映射为带有手动 switch 语句的标记联合。在 Python 中,映射为 带有 isinstance 检查的数据类联合。在 TypeScript 中,映射为对 kind 字段 进行 switch 的判别联合。在所有情况下,模式都是相同的:一个类型,多个变体, 穷尽处理。

第二步:服务器 Actor ​

服务器 Actor 是聊天系统的核心。它作为一个单一 Actor 运行(还记得[第 15 章] ch15的 Actor 吗?),维护两份状态:已连接客户端的映射和消息循环。

管理已连接客户端的服务器 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 来读取输入并将消息转发给服务器。 这就是客户端处理器。

读取输入并解析命令的客户端 Actor

客户端处理器是将用户输入解析并转换为协议消息的地方。它处理以下情况:

  1. /quit —— 发送 Disconnect 消息并从函数返回,这会终止该 Actor。这是 用户干净退出的方式。

  2. /msg <name> <text> —— 将命令解析为目标用户名和消息文本,然后向服务器 发送 PrivateMsg。split(" ") 调用(来自第 21 章的标准库导览) 将输入分成多个部分,parts[2..].join(" ") 将名称之后的所有内容重新组合。

  3. 纯文本 —— 任何不是命令的内容都会作为 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 种语言代码相同,此处不再重复 完整代码块。完整服务器包含以下改进:

  1. 用户名冲突检测 —— Connect 处理器检查 clients.contains(name) 并 拒绝重复的用户名。

  2. 广播函数 —— broadcast 和 broadcast_room 辅助函数向所有已连接客户端 或特定房间的所有成员发送消息。

  3. 欢迎消息 —— 用户连接时,服务器通过 send caller 发送个人欢迎消息, 并向其他人广播其到来。

  4. 私信错误处理 —— PrivateMsg 处理器在发送前检查接收者是否存在。

  5. 客户端命令 —— client_handler 现在支持 /join、/leave、/rooms 和 /msg 命令。

测试服务器 ​

测试聊天服务器有两种方式:手动测试和自动化测试。

手动测试 ​

运行服务器并用多个终端连接:

console
$ automan run
=== AutoChat Server v1.0 ===
Starting on port 8080...
Server running. Press Ctrl+C to stop.

在实际部署中,你会用单独的客户端进程连接。在开发阶段,代码中模拟的客户端 处理器就足够了。

自动化测试 ​

Auto 支持 #[test] 属性进行单元测试。以下是我们会编写的测试:

auto
#[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
}

这些测试在不运行服务器的情况下验证了核心数据操作。

进阶方向 ​

这个聊天服务器是一个坚实的基础。以下是一些扩展思路:

  1. 身份验证 —— 添加 Login(String, String) 消息,在允许连接前检查凭据。 使用 std::crypto 存储密码哈希。

  2. 持久化 —— 使用 std::fs 的 write_file 将聊天记录保存到文件。服务器 重启时用 read_file 加载。

  3. WebSocket 支持 —— 使用 std::net 接受 WebSocket 连接而非原始 TCP, 让浏览器可以直接连接。

  4. 管理员命令 —— 添加 /kick、/ban 和 /create-room 命令,仅限 管理员使用。

  5. 消息历史 —— 为每个房间保存最近 N 条消息,新用户加入时发送给他们。

  6. 输入指示器 —— 添加 Typing(String, String) 消息,向房间其他成员显示 "Alice 正在输入..."。

  7. 速率限制 —— 限制用户每秒可以发送的消息数量以防止垃圾信息。

  8. 加密 —— 使用 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、提取查询参数和收集进程输出。

将 URL 解析为各组件
从 URL 中提取查询参数
使用 List 收集进程输出

总结 ​

这个毕业设计将第三阶段和整本书的每个概念整合在一起:

概念使用位置
枚举Message 枚举定义协议
模式匹配is msg { Connect(name) =&gt; ... }
Mapclients: Map&lt;String, int&gt;、rooms: Map&lt;String, List&lt;String&gt;&gt;
List房间成员列表、消息分片
字符串方法split、join、starts_with、contains、to_string
Actorspawn 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&lt;String, List&lt;String&gt;&gt;、Message 变体

核心要点:

  1. 先设计协议 —— Message 枚举是所有组件之间的契约。把它设计好, 其他一切都会顺畅。

  2. 一个 Actor 拥有状态 —— 服务器 Actor 是客户端和房间的唯一真相来源。 这消除了整类并发 bug。

  3. 逐步构建 —— 我们从消息类型开始,然后是服务器,然后是客户端处理器, 接着是房间,最后是完整程序。每一步都可以独立测试。

  4. Actor 模型天然可扩展 —— 添加更多功能(认证、持久化、管理命令)意味着 添加更多消息类型和更多处理程序。架构保持不变。

  5. 错误处理不是事后补丁 —— 我们检查了重复的用户名、缺失的用户和不存在 的房间。这些检查是正常控制流的一部分,而非事后添加。

基于 MIT 许可发布。