简介:基于 Rust 语言实现的 Web 服务器课程设计项目,适合计算机网络专业学生作为期末大作业或课程设计的参考,也面向希望接触系统级编程的 Rust 初学者。压缩包内含 34 个文件,主要包括 8 个 Rust 源文件、6 个 JavaScript 脚本、3 个 HTML 页面、2 个 TOML 配置文件以及 YAML、CSS、MD 等辅助文档,整体仅 523KB,结构清晰、部署简单。源码按功能模块划分,除了主程序外,还包含参数解析、请求处理、配置管理、缓存机制、响应生成与异常处理等独立模块,每个文件均带有注释,能够帮助初学者理清 HTTP 服务从接收请求、解析配置、处理业务到构造响应的完整链路。此外,附带的 README 与环境配置示例说明了运行方式,学习路径十分友好。目前已有 263 人浏览学习,适合需要快速完成高质量课程设计并想巩固 Rust 网络编程能力的读者。
1. 计算机网络课程设计选 Rust 写 Web 服务器,这套源码+文档方案值得你从头复现一遍
拿到「基于 Rust 的 Web 服务器的实现项目源码+文档说明.zip」这个标题,先别急着找压缩包里的现成代码。做计算机网络课程设计,最容易被老师一句话问倒的就是:这里为什么这么写、底层发生了什么。Rust 写 Web 服务器,本质上是用一门内存安全且偏底层的系统级语言,把课本里的 TCP 三次握手、HTTP 报文格式、Socket 编程和并发模型串成一条能演示的请求链路。这篇文章按课程设计的交付标准,把架构拆解、核心代码、并发选型和踩坑经验一次讲透。适合正在做计网课设、想写 Rust 练手、以及答辩前想补课的人照着做,大概两周能交付。
2. 从课设评分表倒推项目骨架:明确功能边界与源码结构
2.1 计网课设真正打分的地方:不是功能花哨,而是协议栈覆盖完整
计算机网络课程设计和生产级 Web 服务器是两回事。老师评分看的是你有没有把课程重点"做出来",而不是你写了多少中间件。我见过不少学生堆了一堆功能——支持 HTTPS、WebSocket、自定义路由框架——结果问到 TCP 半关闭答不上来,分数反而不如一个只做 HTTP/1.1 但链路完整、代码清楚的项目。
一套能拿高分的 Rust Web 服务器课设,至少要覆盖这几层:
- TCP Socket 编程:
bind、listen、accept,理解三次握手在accept返回前后的位置 - HTTP 报文解析:请求行、请求头、空行、报文体,能按
\r\n逐行拆解 - 响应构造:状态行、响应头、Content-Length、
\r\n\r\n分隔 - 静态文件服务:路径映射、MIME 类型、404/403 状态码
- 并发处理:至少有一个线程池或等价机制,不能是单线程串行 accept
选 Rust 本身是个加分项。用match去拆 HTTP 报文非常顺手,Option和Result强制你处理"畸形请求""文件不存在"这类边界,比 C 语言纯靠指针和约定更稳,也比 Python 课设更能回答"底层到底怎么工作"这类问题。代价是借用检查和所有权会拦住你一阵子,这也是标题里“源码+文档说明”里文档部分应该重点交代的。
注意:课设的评分逻辑是"深度优先、广度克制"。把链路做深、把线程池讲透,比堆十个半吊子功能划算得多。
2.2 Cargo 工程骨架:把"能编译"组织成"能答辩"的模块
拿到源码包后,第一件事不是逐行读,而是先把工程结构看清楚。一个适合答辩的 Rust Web 服务器,目录我建议这样拆:
web_server/ ├── Cargo.toml ├── src/ │ ├── main.rs # 程序入口:bind + 启动线程池 │ ├── lib.rs # 对外暴露库接口,便于测试 │ ├── http/ │ │ ├── mod.rs # 请求/响应类型的公共定义 │ │ ├── request.rs # HTTP 请求解析 │ │ └── response.rs # 响应构造与 MIME 映射 │ └── server/ │ ├── mod.rs # 监听循环与连接分发 │ └── threadpool.rs # 线程池实现 └── static/ ├── index.html └── 404.html创建命令和初始依赖:
cargo new web_server cd web_server cargo add anyhow # 可选,课设里也可以不用,标准库的 io::Result 足够我一般在课设里刻意保持零第三方依赖,main.rs、lib.rs、http/、server/四个目录,只用标准库。原因很实在:答辩时老师随时可能指着某行问你"这个 crate 是干什么的",依赖越多越被动。Rust 标准库的TcpListener、TcpStream、mpsc::channel、Arc<Mutex<T>>已经够实现一个完整可用的 Web 服务器,而且这些恰好是系统编程课程里希望学生掌握的机制。
main.rs保持精简,把解析和响应逻辑放进lib.rs。这样你可以在tests/目录里直接写集成测试,不启动网络也能测试解析函数,文档里写测试覆盖率时也更有底气。
2.3 协议实现边界:这些功能必须做,这些明确不做
课设项目最怕的是"什么都想沾一点"。我建议你对齐下面这个取舍表,把它写进课程设计文档的"功能边界"小节,老师会觉得你想清楚过:
| 功能点 | 建议 | 理由 |
|---|---|---|
| HTTP/1.1 请求行解析 | 必做 | 协议核心,解析GET /path HTTP/1.1 |
| Host 头校验 | 必做 | HTTP/1.1 强制要求,缺了直接 400 |
| Content-Length 读取 | 必做 | 处理 POST 报文体和响应长度 |
| URL 百分号解码 | 必做 | 不处理的话中文文件名全部 404,这个特别容易被忽略 |
| MIME 类型映射 | 必做 | 否则浏览器拿不到text/html会直接下载文件 |
| Keep-Alive 连接复用 | 选做 | 做出来是加分项,但会引入读超时复杂度 |
| HTTPS/TLS | 明确不做 | 课设时间有限,且 Rust 侧需要引入 rustls 等依赖 |
| HTTP/2 | 明确不做 | 涉及帧、流控,超出计网课设范围 |
| 路由框架 | 明确不做 | 用match映射路径就够,不必抽象 |
这里有个容易被低估的坑:URL 解码。浏览器访问/你好.html时,发到服务器的请求行里是GET /%E4%BD%A0%E5%A5%BD.html HTTP/1.1,如果你不把%E4%BD%A0还原成 UTF-8 字节,静态文件查找必然 404。很多源码包没做这一步,跑起来访问中文路径就翻车。后面避坑章节我会给出解码代码。
3. 核心链路逐行实现:TCP 监听、HTTP 解析与响应构造
3.1 先跑通最小监听循环:bind、listen 与 accept 背后的状态变化
任何 Web 服务器都从一段原始 TCP 监听开始。先看main.rs里最小可运行的骨架:
use std::io::{Read, Write}; use std::net::{TcpListener, TcpStream}; fn main() -> std::io::Result<()> { // 绑定并监听,内核会维护一个已完成三次握手的连接队列 let listener = TcpListener::bind("127.0.0.1:8080")?; println!("listening on http://127.0.0.1:8080"); // incoming 返回迭代器,每次 accept 取出一个已建立的 TCP 连接 for stream in listener.incoming() { let stream = match stream { Ok(s) => s, Err(e) => { // 单个连接 accept 失败不该拖垮整个服务 eprintln!("accept error: {e}"); continue; } }; handle_connection(stream); } Ok(()) } fn handle_connection(mut stream: TcpStream) { // 用堆上的缓冲区,避免栈上固定数组带来的截断问题 let mut buf = [0u8; 4096]; let n = match stream.read(&mut buf) { Ok(n) => n, Err(e) => { eprintln!("read error: {e}"); return; } }; let request = String::from_utf8_lossy(&buf[..n]); println!("received: {}", request.lines().next().unwrap_or("")); // 先随便回一个响应,让浏览器不再白屏 let body = b"<h1>it works</h1>"; let response = format!( "HTTP/1.1 200 OK\r\nContent-Length: {}\r\n\r\n", body.len() ); // 注意:body 要作为字节写出去,不能用 String 拼接中文 stream.write_all(response.as_bytes()).unwrap(); stream.write_all(body).unwrap(); stream.flush().unwrap(); }这段代码有四个关键细节。第一,bind("127.0.0.1:8080")和bind("0.0.0.0:8080")的区别:前者只接受本机回环请求,后者监听所有网卡接口,课设演示建议用127.0.0.1,避免暴露到局域网产生奇怪的防火墙弹窗。第二,incoming()是阻塞迭代器,它背后就是accept(2)系统调用,每次循环阻塞等待内核把某个客户端的握手队列项返回。第三,read一次最多读 4096 字节,HTTP 请求头通常够用,但 POST 大报文会被截断——这个我在第 5 章讲怎么处理。第四,响应里Content-Length必须和 body 字节数严格一致,少了浏览器一直转圈等数据。
3.2 把字节流解析成 Request 结构体:按 \r\n 拆行的正确姿势
现在把handle_connection里的请求字符串变成结构体,课设源码包的核心价值就体现在这个解析模块。创建一个http/request.rs:
#[derive(Debug, Clone)] pub struct Request { pub method: String, pub path: String, // 解码之后的路径 pub version: String, pub headers: Vec<(String, String)>, pub body: Vec<u8>, } /// 从字节流里解析请求。返回 None 表示请求不完整或格式非法。 pub fn parse_request(buffer: &[u8]) -> Option<Request> { // HTTP 规定按 CRLF 分隔,LF 单独出现时要容忍(很多客户端不规范) let text = String::from_utf8_lossy(buffer); let mut lines = text.split("\r\n"); let request_line = lines.next()?; let mut parts = request_line.split_whitespace(); let method = parts.next()?.to_string(); let raw_path = parts.next()?.to_string(); let version = parts.next()?.to_string(); // 拆分请求头,遇到空行表示 header 区结束 let mut headers = Vec::new(); let mut body_start = 0usize; for line in lines { if line.is_empty() { break; } if let Some((k, v)) = line.split_once(':') { headers.push((k.trim().to_string(), v.trim().to_string())); } body_start += line.len() + 2; // +2 算上 \r\n } // URL 解码,后续用于拼文件路径 let path = percent_decode(&raw_path); Some(Request { method, path, version, headers, body: Vec::new(), // 完整实现里再按 Content-Length 截取 }) } /// 把 %E4%BD%A0 还原为 UTF-8,+ 号还原为空格(常见于 query) fn percent_decode(s: &str) -> String { let bytes = s.as_bytes(); let mut out = Vec::with_capacity(bytes.len()); let mut i = 0; while i < bytes.len() { if bytes[i] == b'%' && i + 2 < bytes.len() { // 解析两个十六进制字符,比如 E4 -> 228 let hi = hex_val(bytes[i + 1])?; let lo = hex_val(bytes[i + 2])?; out.push((hi << 4) | lo); i += 3; } else if bytes[i] == b'+' { out.push(b' '); i += 1; } else { out.push(bytes[i]); i += 1; } } String::from_utf8_lossy(&out).into_owned() }这里有一个课设里几乎人人都踩的边界:split("\r\n")切到请求体时会出问题。HTTP 协议中报文头和 body 之间用空行\r\n\r\n分隔,但 body 本身可能包含换行符。上面为了演示简洁没有单独提取 body,完整版应该在找到空行后,记录空行后面字节的偏移量,再按Content-Length头从原始buffer里截取body[body_start..body_start_len],而不是用String::from_utf8_lossy全程处理——因为 body 可能是二进制图片。
percent_decode里hex_val把字符转成 0-15 的数值,遇到非法序列时这门课设可以直接返回原始字符串或 400。答辩老师常问"为什么不直接用成熟库",你可以回答:课程设计目的是复现协议过程,标准库手写才能展示对 RFC 3986 的理解。
3.3 组装响应:状态行、响应头与静态文件服务的 MIME 映射
解析完请求,下一步是构造响应。文件放在http/response.rs:
use std::collections::HashMap; use std::fs; use std::path::PathBuf; pub fn build_response(status: &str, content_type: &str, body: &[u8]) -> Vec<u8> { let status_line = format!("HTTP/1.1 {}\r\n", status); let headers = format!( "Content-Type: {}\r\nContent-Length: {}\r\n\r\n", content_type, body.len() ); let mut resp = Vec::with_capacity(status_line.len() + headers.len() + body.len()); resp.extend_from_slice(status_line.as_bytes()); resp.extend_from_slice(headers.as_bytes()); resp.extend_from_slice(body); resp } /// 根据扩展名返回 MIME,缺省用 application/octet-stream pub fn mime_for(path: &str) -> &'static str { let ext = match path.rsplit('.').next() { Some(e) => e.to_ascii_lowercase(), None => String::new(), }; match ext.as_str() { "html" => "text/html; charset=utf-8", "css" => "text/css", "js" => "application/javascript", "png" => "image/png", "jpg" | "jpeg" => "image/jpeg", "gif" => "image/gif", "ico" => "image/x-icon", "txt" => "text/plain; charset=utf-8", _ => "application/octet-stream", } } pub fn serve_static(base_dir: &str, request_path: &str) -> Vec<u8> { // 先做路径归一化,防止 .. 穿越目录 let rel = request_path.trim_start_matches('/'); let mut path_buf = PathBuf::from(base_dir).join(rel); if path_buf.is_dir() { path_buf.push("index.html"); } if path_buf.exists() { let body = fs::read(&path_buf).unwrap_or_default(); let ctype = mime_for(&path_buf.to_string_lossy()); build_response("200 OK", ctype, &body) } else { let body = b"<h1>404 Not Found</h1>"; build_response("404 Not Found", "text/html; charset=utf-8", body) } }build_response的关键是Content-Length必须等于body.len()。很多源码包在返回静态文件时把Content-Length写死成 0 或用字符串长度代替字节长度,一旦文件里有中文,浏览器就只显示一半内容。这是一个很经典的“黑匣子”现场:网页乱码,抓包看没问题,其实是字节长度算错了。
serve_static里用trim_start_matches('/')去掉开头的/,然后拼到base_dir后。路径穿越是课设里最常见的攻击性问题:如果用户请求/../Cargo.toml,PathBuf::join会把路径拼到项目根目录之外。防御写法是用components()过滤掉ParentDir,我在避坑章会给完整限制版。MIME 表用match而不是HashMap::from,主要是编译期就能确认所有分支有返回值,而且答辩时解释起来更直观。
4. 并发是这套代码的得分点:手写线程池处理多连接
4.1 为什么“每连接开一个线程”在课设里就是翻车样板
监听循环一旦直接接上std::thread::spawn每次来处理连接,代码确实最简单,但有两个会让答辩现场难堪的问题。第一,默认线程栈大小在多数桌面系统上是 2MB 左右,每连接一线程意味着每来个请求占 2MB 虚拟内存,ab压测到几百并发时内存立刻吃紧;第二,创建线程本身是重量级系统调用,高并发下线程切换开销占比飙升,老师问"这个方案能撑多少 QPS"时你答不上来。
常见的折中方案是线程池:提前创建固定数量线程,用队列把连接任务丢给空闲线程。Rust 标准库没有现成的线程池,num_cpus这种 crate 能帮你拿到 CPU 核数,但课设里我更推荐直接手写一个。
4.2 一个最小线程池的完整实现:mpsc 通道 + Worker 线程
下面这段是经过精简但能直接编译运行的线程池,放在server/threadpool.rs:
use std::sync::mpsc::{self, Receiver, Sender}; use std::sync::{Arc, Mutex}; use std::thread::{self, JoinHandle}; type Job = Box<dyn FnOnce() + Send + 'static>; pub struct ThreadPool { workers: Vec<Worker>, sender: Sender<Job>, } impl ThreadPool { pub fn new(size: usize) -> Self { assert!(size > 0); let (sender, receiver) = mpsc::channel(); let receiver = Arc::new(Mutex::new(receiver)); let mut workers = Vec::with_capacity(size); for id in 0..size { // 每个 worker 持有一个 receiver 的锁保护引用 workers.push(Worker::new(id, Arc::clone(&receiver))); } ThreadPool { workers, sender } } pub fn execute<F>(&self, f: F) where F: FnOnce() + Send + 'static, { let job = Box::new(f); // send 返回 Err 说明所有 worker 已退出,服务已不可用 let _ = self.sender.send(job); } } struct Worker { id: usize, thread: Option<JoinHandle<()>>, } impl Worker { fn new(id: usize, receiver: Arc<Mutex<Receiver<Job>>>) -> Self { let thread = thread::spawn(move || loop { // 先释放锁再执行任务,否则会阻塞其他 worker 取任务 let job = receiver.lock().unwrap().recv().unwrap(); job(); }); Worker { id, thread: Some(thread) } } }这段代码有三个答辩必问点,你要能讲明白。第一,mpsc::channel是"多生产者单消费者",但通过把Receiver包进Arc<Mutex<_>>并克隆给所有 worker,就把单消费者变成了"互斥锁保护的多个消费者",每个任务只会被一个线程取出——这是线程池最核心的设计思路。第二,dyn FnOnce() + Send + 'static这个类型约束:Send表示闭包能安全地跨线程移动,'static表示闭包里不能借用线程池自身的生命周期变量,所以调用execute时如果闭包里要使用stream,必须move进去。第三,recv()是阻塞的,worker 线程空闲时挂在这个调用上,不占 CPU;等任务来了由内核唤醒。
在main.rs里把线程池接进监听循环:
let pool = ThreadPool::new(4); // 本机 CPU 逻辑核心数,课设机器一般 4-8 之间 let listener = TcpListener::bind("127.0.0.1:8080")?; for stream in listener.incoming() { let stream = match stream { Ok(s) => s, Err(e) => { eprintln!("accept error: {e}"); continue; } }; // 这里必须 move,把 stream 所有权移入闭包 pool.execute(move || { handle_connection(stream); }); }ThreadPool::new(4)这个 4 怎么定?我的经验是取本机 CPU 逻辑核数即可,压测时调到 8 看曲线。线程池小于 2 时多个浏览器标签页同时刷很容易看到排队等待导致白屏;大于 16 时课设机器上收益递减,还显得你只是堆资源,没理解并发模型。
4.3 连接排队与任务调度的直观效果:用日志验证并发
接上线程池后,建议在handle_connection里打印线程 id 和请求路径:
fn handle_connection(mut stream: TcpStream) { let tid = std::thread::current().id(); // ... println!("[worker {:?}] {} {}", tid, method, path); // ... }然后开两个终端,一个连续刷新浏览器,另一个用下面的命令并发请求:
for i in $(seq 1 20); do curl -s http://127.0.0.1:8080/ > /dev/null & done观察终端输出,你会看到不同的线程 id 交替处理请求,这就是"并发"在进程内的直观证据。这个演示在答辩时非常有说服力:老师看到的不是你说的并发,而是日志里确实有 4 个 worker 在分担请求。同时这也是验证线程池是否生效的最快方法。如果所有请求都打在同一个线程 id 上,检查你是不是把线程池创建在了for循环内部——每次都新建池子等于没有池。
再强调一个细节:execute里我用let _ = self.sender.send(job)忽略错误。完整实现里这里应该返回Result,当 worker 全部 panic 退出时能感知服务不可用。课设阶段可以先忽略,但要在文档里注明"生产环境必须处理 send 失败",老师会觉得你考虑过边界。
5. 避坑手册:Rust Web 服务器实现中最容易翻车的 5 个细节
5.1 中文路径百分号编码后全部 404
现象:浏览器访问/static/课程设计.html直接 404,但用英文文件名一切正常。
原因:客户端发送的请求行里中文被编码成%E8%AF%BE%E7%A8%8B...,服务器拿原始编码后的字符串直接拼文件路径,系统里根本没有叫%E8%AF%BE的文件。
解决:在第 3 章percent_decode那段基础上,把它应用到serve_static的路径参数上,并把解码后的字符串返回给Request.path。注意别在parse_request里解码两次——如果原始路径没有百分号,解码后内容不变,但有的按半个字节去 decode 会把%残留成乱码。建议只在从raw_path到path这一步解码一次,后续拼路径用解码后的值。
5.2 浏览器转圈不返回:Content-Length 和实际字节数不一致
现象:页面一直 loading,curl不退出,等了很久才超时。
原因:HTTP/1.1 默认Keep-Alive,响应体长度由Content-Length头决定。如果实际写出去的 body 是 1024 字节,头里写的却是 100,浏览器会一直等剩下的 24 字节;反之写多了,下一次解析响应从错位位置开始,直接乱码。
解决:所有响应头里的Content-Length一律从body.len()取字节数,不要手写常量。尤其静态文件用fs::read读完后,先算长度再拼响应头。写代码时用一个统一的build_response函数,不要到处手动格式化响应字符串。
5.3 线程池某个 worker panic 后,服务越来越慢直到卡死
现象:压测到一半,日志里某两个线程 id 消失了,后续请求全部超时。
原因:handle_connection里某个分支直接unwrap()了一个None,线程 panic,thread::spawn的闭包退出。因为mpsc::Receiver还锁在被清理的锁上吗?不是——更隐蔽的是那个 worker 的recv().unwrap()只有在所有Sender都 drop 时才会返回 Err,main里监听循环还活着,发送端还在,所以其他 worker 会一直阻塞在recv上等着,少一个 worker 少一份处理能力。
解决:worker 里的recv()拿到任务后,用一个不可 panic 的边界处理起来:match job_result,Err 时 break 退出;任务执行函数内部把可能失败的操作都换成match,不轻易unwrap。给ThreadPool加一个Drop实现,drop 时通知所有 worker 退出并join,这样服务关闭时资源干净回收,文档里也写得清楚。
5.4 请求体超过 4096 字节被截断,POST 内容残缺
现象:用curl -d @large.jsonPOST 一个 10KB 数据,服务端解析出来的 body 只有前 4096 字节。
原因:第 3 章里我展示了let mut buf = [0u8; 4096],一次read只读到了流的前 4096 字节,没有继续把剩余数据读完。TcpStream::read返回的是本次读取到的字节数,不保证读完整一个报文。
解决:解析时不要直接从固定缓冲区里抠 body,而是先解析 header,拿到Content-Length: N之后,循环read直到累计读到 N 字节,或者连接关闭。课设里可以限制最大请求体大小为 1MB,超过就返回 413 Payload Too Large,这个限制值本身也是答辩可讲的话题。
5.5 路径穿越:请求 /../Cargo.toml 直接把项目源码暴露了
现象:浏览器访问/../Cargo.toml或/../../etc/hosts,服务器返回了文件内容。
原因:PathBuf::from(base_dir).join(request_path)直接把..当有效路径处理,Unix 系统下join不会自动阻止上级目录。
解决:解析完路径后过滤掉所有ParentDir组件。用标准库的Path::components()遍历,跳过Component::ParentDir,再把剩余组件重新拼成PathBuf。同时把拼接好的规范路径和base_dir做前缀校验:目标路径必须以base_dir开头,否则直接返回 403。课程设计文档里写下这一条,老师对你的印象会明显不一样。
提示:以上五个坑的共性,是所有源码类课设都会遇到的现象——表面是 Rust 代码编译不过,本质是对协议边界理解不完整。建议把这五条全部写进课程设计文档的“问题与解决”章节,比抄一段原理更有说服力。
6. 交作业前的验证清单与答辩加分技巧
写完全部代码,先用一组命令做端到端自测,这一步能帮你提前暴露七八成问题。我一般按这个顺序跑:
# 1. 静态文件与状态码 curl -v http://127.0.0.1:8080/index.html curl -i http://127.0.0.1:8080/nonexist.html # 2. 中文路径 curl -i --path-as-is "http://127.0.0.1:8080/%E4%BD%A0%E5%A5%BD.html" # 3. POST 请求体 curl -i -d 'name=test&age=18' http://127.0.0.1:8080/submit # 4. 路径穿越防护 curl -i --path-as-is "http://127.0.0.1:8080/../Cargo.toml" # 5. 并发压测(连接数和请求数按本机能力适度调) ab -n 1000 -c 50 http://127.0.0.1:8080/curl -v会打印完整请求和响应头,出现Connected说明 TCP 握手成功,响应头里Content-Length和实际接收字节数不一致时立刻就能看出来。ab压测如果Failed requests不为 0,优先去日志里查是不是某个 worker panic 或路径穿越防护误伤了正常请求。
答辩时不要从main.rs第一行开始讲,面试官没耐心听代码逐行走。我习惯只讲一条链:accept拿到 TCP 连接 → 按 HTTP 协议解析请求行和头部 → 查表映射静态文件路径 → 从线程池取一个空闲 worker 执行响应构造与写出。每个环节只强调一个关键决定——比如为什么线程池大小取 CPU 核数、为什么Content-Length必须用字节长度、为什么路径要做穿越过滤。讲清楚这三个决定,课设基本就稳了。
如果时间富余,可以再做两个进阶点,但注意控制范围:一是给TcpStream设置读超时,让 Keep-Alive 的空闲连接能被清理;二是把线程池的任务类型从闭包改成一个trait对象,让任务分发和业务逻辑解耦。这两点都能放进文档的“改进方向”里,但不要为它们无限加班——课程设计的及格线是“协议正确、并发合理、文档可信”,而不是成为一个生产级服务器。
我自己的教训是:写课设时最爱在“要不要加个缓存”上纠结,结果缓存没写好,反而把基础链路弄崩了。其实老师最想看到的恰恰是你把最基本的东西做扎实。希望这篇能帮你少走一段弯路,交出一份自己心里有底的作业。
本文还有配套的精品资源,点击获取