当前位置: 首页 > news >正文

趣味学RUST基础篇(实战Web server)完结

“呼……终于把前面那些忍术都学会了。”小新擦了擦汗,合上厚厚的《Rust 忍者秘籍》。但就在这时,书的最后一页突然金光一闪,浮现出一行字:

“真正的忍者,不仅要会用工具,更要亲手打造工具!来吧,少年,建造你自己的 Web 服务器!”

小新瞪大了眼睛:“Web 服务器?那不是爸爸用来工作的大电脑吗?我也能做?”秘籍温柔地回答:“当然!今天,我们就用学到的所有知识,从最底层开始,亲手搭建一个能返回 ‘Hello, 动感超人!’ 的网站!就像搭积木一样简单!”

认识“网络电话线”——TCP 和 HTTP

小新挠头:“服务器是啥?它怎么和我的浏览器说话?”

秘籍拿出一张图:

[你的浏览器] <--(打电话)--> [Web 服务器]

“想象一下,服务器就像一个永不挂机的‘客服中心’。它用一根叫 TCP 的‘超级电话线’一直开着,等着别人打进来。”

当你在浏览器输入 http://localhost:8080 并按下回车,你的电脑就拨通了这根“电话线”。

接着,你说的话必须用一种客服能听懂的“暗号”——这就是 HTTP 协议

比如,你说:“你好,请给我首页!”(HTTP 请求)
客服(服务器)回答:“好的,这是你要的 ‘Hello, 动感超人!’”(HTTP 响应)

小新立志要建造自己的“动感超人”网站。秘籍说:“别急,我们先造个最简单的版本——一个只有一个接线员的客服中心。”

小新问:“只有一个?那要是很多人打电话,不就忙不过来了吗?”

秘籍点点头:“没错!它会很慢。但别忘了,我们的目标是学习原理,而不是追求速度。就像学骑自行车,先学会平衡,再考虑装火箭推进器!”


第一步:开通“电话专线”

小新首先要申请一条专属的“电话线”(TCP Socket),并指定一个“分机号”(端口号)。

use std::net::TcpListener;fn main() {// 开通一条电话线,绑定到 7878 分机let listener = TcpListener::bind("127.0.0.1:7878").unwrap();println!("动感超人单线客服中心已启动!拨打 http://127.0.0.1:7878 即可联系!");
}

TcpListener::bind("127.0.0.1:7878") 就像去电信局报备:“我要在本地(127.0.0.1)开一个 7878 号分机。” unwrap() 的意思是:“如果开不了,我就当场哭给你看!”(程序崩溃)。


第二步:接起第一个电话

现在电话线通了,但没人接。小新需要一个“永不下班的接线员”,一直等着电话响。

// 让接线员开始工作,循环接听每一个打进来的电话for stream in listener.incoming() {let tcpStream = stream.unwrap(); // 接起电话!println!("叮铃铃!有客人来电!");// TODO: 跟客人说话...}

incoming() 方法返回一个“电话铃声迭代器”。每次有电话打进来,循环就会执行一次,(Stream)就是这次通话的“连接通道”。


第三步:读懂客人的“暗语”(HTTP 请求)

客人打来电话,说的话都是加密的“HTTP 暗语”。小新需要一个“翻译本”来破译。

use std::io::prelude::*;// 准备一个笔记本(缓冲区)来记录客人说的话
let mut 缓冲区 = [0; 512];.read(&mut 缓冲区).unwrap(); // 把客人的话写进笔记本// 用翻译本(UTF-8)把二进制代码转成文字
let 请求 = String::from_utf8_lossy(&缓冲区[..]);
println!("客人说:\n{}", 请求);

完整代码

use std::io::Read;
use std::net::TcpListener;fn main() {// 开通一条电话线,绑定到 7878 分机let listener = TcpListener::bind("127.0.0.1:7878").unwrap();println!("动感超人单线客服中心已启动!拨打 http://127.0.0.1:7878 即可联系!");// 让接线员开始工作,循环接听每一个打进来的电话for stream in listener.incoming() {let mut tcp_stream = stream.unwrap(); // 接起电话!println!("叮铃铃!有客人来电!");// 准备一个笔记本(缓冲区)来记录客人说的话let mut buffer = [0; 512];tcp_stream.read(&mut buffer).unwrap(); // 把客人的话写进笔记本// 用翻译本(UTF-8)把二进制代码转成文字let request = String::from_utf8_lossy(&buffer[..]);println!("客人说:\n{}", request);}
}

运行程序,打开浏览器访问 http://127.0.0.1:7878,控制台会打印出类似这样的内容:

客人说:
GET / HTTP/1.1
Host: 127.0.0.1:7878
Connection: keep-alive
Cache-Control: max-age=0
sec-ch-ua: "Not;A=Brand";v="99", "Google Chrome";v="139", "Chromium";v="139"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "macOS"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v
叮铃铃!有客人来电!

小新恍然大悟:“原来 GET / 就是‘请给我首页’的意思!后面的那些是浏览器的自我介绍。”


第四步:说出“标准回复”(HTTP 响应)

现在轮到小新当客服了!他必须按照“HTTP 回复手册”来回答,否则客人(浏览器)会看不懂。

// 构造一个标准的 HTTP 响应
let response = "HTTP/1.1 200 OK\r\nContent-Length: 13\r\n\r\nHello, xiaoxin!";// 把回复大声说出来(写入连接)
tcp_stream.write(response.as_bytes()).unwrap();
tcp_stream.flush().unwrap(); // 说完记得“挂电话”(刷新缓冲区)

这里的 HTTP/1.1 200 OK 是“通话成功”的暗号。
Content-Length: 13 告诉浏览器:“我说的话一共 13 个字节长,你准备接收。”
中间的 \r\n\r\n 是“下面就是正文”的分隔符。 最后才是真正的内容:Hello, xiaoxi!

刷新浏览器!哇!屏幕上真的出现了“Hello, xiaoxi!”

第五步:代码重构

当前的接线员小新是这样工作的:

  1. 准备一个大笔记本[0; 512])。
  2. 客人一说话,他就疯狂记录,不管客人说一句话还是说了一本书,他都拼命往笔记本上写,直到写满 512 个字(字节)或者客人说完了。
  3. 问题来了
    • 浪费纸:如果客人只说了“你好”两个字,笔记本剩下 510 个字都是空白,多浪费啊!
    • 看不懂话:笔记本上记的全是乱码般的二进制数字。小新得自己费劲地把这 512 个数字翻译成文字,还得自己判断哪里是句子的结束(比如 \r\n)。
    • 效率低:他得一次性处理完所有数据,像个搬运工,把整块数据搬过来再分析。

现在,小新升级了!升级成为了超级智能的“语音转文字机器人”(BufReader),并学会了“逐行听取”(lines())的技巧。

fn handle_connection(mut stream: TcpStream) {let buf_reader = BufReader::new(&stream); // 给接线员配一个“语音转文字机器人”let http_request: Vec<_> = buf_reader.lines() // 机器人把客人说的话,自动按“行”切分好.map(|result| result.unwrap()) // 把每一行的“结果”(Ok(line))解开,拿到真正的文字.take_while(|line| !line.is_empty()) // 关键!只要听到“空行”,就停止!因为HTTP头结束了!.collect(); // 把所有行收集到一个列表里println!("Request: {http_request:#?}"); // 清晰地打印出每一行
}
新方法的好处:
  1. 只读需要的部分(高效节能)

    像一个聪明的侦探,只关注“HTTP 请求头”部分。一旦遇到空行\r\n\r\n),就立刻知道:“头信息结束了,后面是正文(body),我现在不需要!” 然后停止读取。这大大节省了资源和时间。

  2. 自动分行,清晰易读

    • 新方法lines() 方法自动把数据按行分割。http_request 是一个 Vec<String>,每一行都是一个独立的 String。打印出来是这样的:
      Request: ["GET / HTTP/1.1","Host: 127.0.0.1:7878","User-Agent: Mozilla/5.0 ...",// ... 其他头部
      ]
      
      一目了然!就像把一整段话,自动分成了一个个句子。
  3. 自动处理编码和错误

    • BufReader::lines() 返回的是 io::Result<String>。它内部已经帮你处理了从字节(u8)到 UTF-8 字符串的转换。map(|result| result.unwrap()) 虽然简单粗暴(遇到编码错误会崩溃),但至少它把“解码”这个脏活累活干了,你拿到的就是“干净的文字”。
  4. 内存使用更合理

    • 旧方法:总是分配 512 字节的数组,即使请求很小。
    • 新方法Vec<String> 的大小是动态的,只根据实际的请求头行数和每行长度来分配内存,更加灵活和高效。
  5. 逻辑更清晰,更符合 HTTP 协议

    • HTTP 协议明确规定:请求头和请求体之间用一个空行分隔。新方法的 .take_while(|line| !line.is_empty()) 完美地遵循了这一规范,体现了“协议意识”。代码本身就说明了“我只处理到空行为止”。

将代码从直接 read 改为使用 BufReaderlines(),是一次从“原始蛮力”到“优雅智能”的升级:

  • BufReader:提供了缓冲,减少了系统调用次数,提高了 I/O 效率。
  • lines():提供了行导向的读取,自动分割,语义清晰。
  • take_while + 空行判断:精准地截取了 HTTP 请求头,符合协议,高效且安全。

这不仅让代码更简洁、更易读、更高效,也让我们对 HTTP 协议的理解更深了一层。这才是“专业接线员”的正确打开方式。

小新的反思:单线程的“甜蜜”与“烦恼”

小新看着成功的页面,很开心。但他很快发现了问题:

  1. 甜蜜之处

    • 简单明了:代码逻辑清晰,每一步都看得见摸得着。
    • 易于理解:完美展示了 Web 服务器的核心流程:监听 -> 接收连接 -> 读请求 -> 写响应。
  2. 烦恼之处

    • 超级慢:想象一下,如果小新正在给第一个客人回话,第二个、第三个客人打来电话,他们只能听到“嘟——嘟——”的忙音!因为接线员(主线程)正忙着呢,根本顾不上别的电话。
    • 效率低下:即使第一个客人只是来看一眼,小新也要花时间处理完他的请求,才能接下一个。这就像银行里只有一个窗口,后面排了一长队。

小新叹了口气:“这样不行啊!我的粉丝们会等得睡着的!”


章鱼小新的烦恼:官网卡成树懒!

小新成功搭建了网站,粉丝们兴奋地涌来。但很快,他发现了一个致命问题

“救命啊!只要有一个粉丝请求‘观看动感超人最新剧集’,后面所有请求‘查看小新头像’的粉丝都得干等!我的网站卡得像树懒在跳慢动作舞!”

这就像一个只有一个窗口的银行:前面一个人要办理复杂的贷款业务,后面的人就算只是取个100块,也得眼巴巴地等上一小时!


第一步:重现“卡死”现场

小新决定做个实验,证明问题有多严重。

他修改了代码,加了一个“休眠5秒”的特殊请求 /sleep

fn handle_connection(mut stream: TcpStream) {// ... 解析请求 ...let (status_line, filename) = match &request_line[..] {"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),"GET /sleep HTTP/1.1" => {thread::sleep(Duration::from_secs(5)); // 让服务器“睡大觉”5秒!("HTTP/1.1 200 OK", "hello.html")}_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),};// ... 发送响应 ...
}

实验开始!

  1. 小新先打开一个浏览器,访问 /sleep
  2. 紧接着,他立刻打开另一个浏览器,访问 /(主页)。

结果:

  • /sleep 页面:5秒后才加载出来。
  • / 页面:竟然也等了整整5秒才出现!明明它应该瞬间加载!

“啊!我的网站是‘一人卡,全员等’的模式!这不行!” 小新急得直跺脚。


第二步:寻找救星——“忍者分身术”(线程池)

《Rust 忍者秘籍》再次发光:“少年,是时候召唤‘影分身之术’了!别让一个任务卡住所有人!”

线程池”(Thread Pool)的概念登场了:

“想象你有 4 个分身。他们不是‘随叫随到’(创建线程),而是提前就坐在客服大厅里待命。每当有电话(请求)进来,一个空闲的分身立刻跳起来接!处理完,他立刻回来休息。这样,最多能同时处理 4 个请求,效率翻了4倍!”

小新恍然大悟:“原来如此!我不能每次来电话都现场‘变身’,太费查克拉了!得提前准备好‘待命小队’!” 小新撸起袖子,开始建造他的“客服大厅”。

1. 核心部件:任务队列和“魔法传声筒”
  • 任务队列 (Task Queue):一个公共的“点菜单”。所有打进来的电话请求,都被写成“服务单”,扔进这个菜单。
  • 魔法传声筒 (Channel):Rust 的 mpsc::channel()。它有两个口:
    • 发送口 (Sender):主线程(小新本体)用它把“服务单”塞进“点菜单”。
    • 接收口 (Receiver):每个分身(工作线程)用它从“点菜单”里抢任务。
2. 召唤“待命小队”

小新变出 4 个分身,并给他们下达指令:

// 1. 创建“魔法传声筒”
let (sender, receiver) = mpsc::channel(); // sender 给主线程,receiver 给工作线程// 2. 变出4个分身,并让他们待命
for id in 0..4 {let 接收者 = receiver.clone(); // 每个分身都需要一个“接收口”(用 Arc<Mutex<>> 包装,稍后解释)thread::spawn(move || { // 变出分身loop { // 分身进入“待命”循环let 任务 = 接收者.recv().unwrap(); // 一直等,直到抢到一个任务println!("分身 {} 开始处理任务", id);任务(); // 执行任务(比如 handle_connection)println!("分身 {} 完成任务", id);}});
}
3. 主线程:接听电话,分发任务

原来的“单线程接线”变成了“调度中心”:

forin 监听器.incoming() {let=.unwrap();// 不再自己处理!而是把任务包装好,扔进“传声筒”!let 任务 = || handle_connection(); sender.send(任务).unwrap();
}

第三步:解决关键难题——“共享接收口”

小新遇到了一个大麻烦:receiver(接收口)只能被一个分身使用!如果他想把 receiver 给4个分身,Rust 编译器会愤怒地报错:use of moved value: receiver

“这就像只有一个‘点菜单’,但4个分身都想同时看它,会乱成一锅粥!”

秘籍解法:Arc<Mutex<T>>——“防抢保护罩”

  • Mutex<T>:给“点菜单”加一把。谁要看菜单,必须先拿到钥匙(lock),看完再还回去。保证同一时间只有一个人能看。
  • Arc<T>:“原子引用计数”,一个魔法护符。戴上它,就能让多个分身共享同一个“点菜单”,并且系统会自动管理“点菜单”的生命周期(没人用了就销毁)。
let (sender, receiver) = mpsc::channel();
let 接收者 = Arc::new(Mutex::new(receiver)); // 给接收口加上“防抢保护罩”for id in 0..4 {let 接收者 = Arc::clone(&接收者); // 每个分身拿到一个“共享的、带锁的接收口”thread::spawn(move || {loop {// 关键:先拿锁,再取任务let 任务 = 接收者.lock().unwrap().recv().unwrap();println!("分身 {} 开始处理任务", id);任务(); // 执行任务}});
}

现在,4个分身可以安全、有序地从同一个“点菜单”里抢任务了!


第五步:实战!让网站飞起来!

小新把所有代码打包成一个酷炫的 ThreadPool 结构体:

pub struct ThreadPool {workers: Vec<Worker>,sender: mpsc::Sender<Job>,
}impl ThreadPool {pub fn new(size: usize) -> ThreadPool {// ... 创建 channel,变出分身,返回线程池 ...}pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{// 把闭包包装成 Job,扔进 senderlet job = Box::new(f);self.sender.send(job).unwrap();}
}

主函数变得异常简洁:

let pool = ThreadPool::new(4); // 雇一个4人客服团队for stream in listener.incoming() {let stream = stream.unwrap();pool.execute(|| {handle_connection(stream); // 这个任务会被扔进队列,由某个分身执行});
}

完整代码

use std::io::{BufRead, BufReader};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex, mpsc};
use std::thread;struct Worker {id: usize,thread: thread::JoinHandle<()>,
}
pub struct ThreadPool {workers: Vec<Worker>,sender: mpsc::Sender<Job>,
}type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {// --snip--pub fn new(size: usize) -> ThreadPool {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 {workers.push(Worker::new(id, Arc::clone(&receiver)));}ThreadPool { workers, sender }}pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.send(job).unwrap();}
}impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {let thread = thread::spawn(move || {while let Ok(job) = receiver.lock().unwrap().recv(){println!("Worker {id} got a job; executing.");job();}});Worker { id, thread }}
}fn main() {let listener = TcpListener::bind("127.0.0.1:7878").unwrap();let pool = ThreadPool::new(4);for stream in listener.incoming() {let stream = stream.unwrap();pool.execute(|| {handle_connection(stream);});}
}fn handle_connection(mut stream: TcpStream) {let buf_reader = BufReader::new(&stream); // 给接线员配一个“语音转文字机器人”let http_request: Vec<_> = buf_reader.lines() // 机器人把客人说的话,自动按“行”切分好.map(|result| result.unwrap()) // 把每一行的“结果”(Ok(line))解开,拿到真正的文字.take_while(|line| !line.is_empty()) // 关键!只要听到“空行”,就停止!因为HTTP头结束了!.collect(); // 把所有行收集到一个列表里println!("Request: {http_request:#?}"); // 清晰地打印出每一行
}

如何理解execute

    pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.send(job).unwrap();}

场景设定:魔法外卖驿站

小新的火锅店除了可以堂食还可以外卖,火锅店旁边修建了一个超厉害的“闪电外卖驿站”。它不是普通的驿站,而是一个由魔法师魔法传送阵组成的高科技系统!

  • 你(顾客):想吃东西,就大喊一声:“来一份 || 番茄锅套餐!”(这就是你的任务)。
  • 驿站站长(ThreadPool:是个和蔼的老爷爷,他手下有 4 个待命的魔法学徒(Worker 线程)
  • 任务投递箱(sender:驿站门口有一个闪闪发光的箱子。你把“订单”扔进去,任务就完成了。

现在,我们来看 execute 方法——这是站长的“接单秘术”!

pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.send(job).unwrap();}
第一步:顾客喊出订单 f
pub fn execute<F>(&self, f: F)
  • 你大喊:“execute(|| 来一份番茄锅套餐!)”。
  • 站长(&self)耳朵一动:“哦!有订单来了!”
  • 他听到了你的具体要求 f —— “做个番茄锅套餐!”。这个 f 就是一个魔法咒语(闭包),念出来就能变出番茄锅套餐。
第二步:站长的“接单规矩”(where 从句)
whereF: FnOnce() + Send + 'static,

站长不是什么订单都接的!他有三条铁律

  1. FnOnce() —— “一次咒语”规则

    • “你这个咒语,必须是念一次就消失的那种!比如‘变出一个番茄锅套餐’,念完番茄锅套餐就出现了,咒语能量耗尽。”
    • 他不接“永久生效”的咒语(比如 Fn)或“需要反复修改”的咒语(FnMut)。任务做完就没了,不能反复做!
  2. Send —— “可传送”规则

    • “你的咒语必须是能用魔法卷轴写下来,并且能安全邮寄的!”
    • 有些咒语太脆弱,一传送就失效(比如依赖你家厨房的魔法锅)。这种他不接。必须是“自给自足”的咒语。
  3. 'static —— “无依赖”规则

    • “你的咒语**不能说‘用我家的锅’、‘用我家的肉’**这种话!”
    • 咒语必须自带所有材料(比如“用我魔法袋里的番茄和肥牛”),或者用驿站提供的材料。不能依赖你家的、随时可能消失的东西。这样学徒在任何地方都能执行。
第三步:把咒语装进“万能魔法卷轴”(Box::new)
let job = Box::new(f);
  • 站长点点头:“嗯,你的‘做个番茄锅套餐’咒语符合规矩!”
  • 他拿出一个金色的万能魔法卷轴(Box<dyn FnOnce>
  • 把你的口头咒语 f “唰”地一下,刻写到这个卷轴上!
  • 为什么用“万能卷轴”?
    • 因为顾客的咒语五花八门:“做个薯条”、“煮碗面”……形状大小都不一样。
    • 这个“万能卷轴”就像一个标准化的快递信封,不管里面是什么任务,装进去后都变成统一大小、统一格式,方便驿站处理。
第四步:扔进“魔法传送阵投递箱”(send)
self.sender.send(job).unwrap();
  • 站长拿着这个写好咒语的“万能卷轴”(job),走到驿站门口的魔法传送阵投递箱(sender
  • “啪!”一声,把卷轴扔了进去。
  • 神奇的事情发生了
    • 投递箱“嘀”地一闪,卷轴瞬间消失!
    • 它被自动传送到驿站深处,掉进了所有学徒都能看到的“待办任务池”里。
  • unwrap() 是什么?
    • 站长非常自信:“我的驿站怎么可能出问题?!”
    • 如果投递箱坏了(比如驿站关门了),他就会大叫一声:“啊!出大事了!”(panic),直接晕倒。

后续故事:任务被执行

  • 驿站深处,4 个待命的魔法学徒(Worker 线程) 正在打盹。
  • 突然,任务池里多了一个“做个番茄锅套餐”的卷轴!
  • 一个机灵的学徒立刻跳起来:“我来!” 他抢过卷轴,念动咒语。
  • “轰!” 一个香喷喷的番茄锅套餐出现在他手上。
  • 他把番茄锅套餐通过另一个传送阵送到你家门口。任务完成!

execute 方法就是“接单-装卷轴-投传送箱”三部曲!

  1. 接单:站长听到你的任务 f
  2. 验货:检查任务是否符合“一次咒语、可传送、无依赖”三大规矩。
  3. 装箱:把任务刻进“万能魔法卷轴”(Box),变成标准快递件。
  4. 投递:扔进“魔法传送阵”(channel),让学徒(工作线程)去抢着做。

好的!让我们把这篇关于 “优雅关机与清理” 的技术文章,变成一个轻松有趣的 “魔法驿站大结局” 故事!


魔法驿站大结局:如何体面地关门打烊?

站长爷爷(ThreadPool)手下有 4 个魔法学徒(Worker 线程),他们通过一个魔法传送阵(channel)接收顾客的“万能魔法卷轴”(任务),然后飞快地做出火锅、小酥肉、薯条。

但有一天,小镇要举办“月光节”,所有店铺都要在晚上 8 点准时关门!站长爷爷犯愁了:

“哎呀!如果我直接把驿站大门一锁,那些正在做火锅的学徒怎么办?他们手里的卷轴还没念完呢!而且,我直接锁门,传送阵会‘啪’地爆炸,学徒们也会被炸飞的!这太不体面了!”

这就是我们今天要解决的问题——如何让驿站“优雅地关门打烊”?


第一幕:粗暴关门的灾难

一开始,站长爷爷是这么干的:

  1. 直接锁门!ctrl-c 终止主线程)
  2. 结果:
    • 正在念咒语的学徒:“啊!咒语中断了!我的肥牛半生不熟!”(任务执行到一半被强行终止)
    • 传送阵“轰”地炸了,碎片乱飞!(channel 被强制关闭,可能造成数据损坏)
    • 所有学徒一脸懵:“老板呢?怎么突然黑天了?”(线程被强制终止)

结局:惨不忍睹!顾客投诉、学徒抱怨、驿站名声扫地。


第二幕:站长爷爷的智慧——优雅关机计划!

站长爷爷是个聪明人,他制定了一个 “三步优雅关店计划”

第一步:先关掉“订单投递箱”!
drop(self.sender.take());
  • 行动:站长爷爷走到门口,把那个闪闪发光的“魔法传送阵投递箱”(sender)小心翼翼地收了起来take),然后轻轻地捏碎drop)。
  • 效果
    • 顾客再也不能投递新订单了!(新请求被拒绝)
    • 传送阵没有爆炸,而是安静地熄灭了。(channel 被正常关闭)
    • 驿站深处的学徒们感觉到:“咦?传送阵没信号了!看来新订单停止了!”
第二步:给学徒们发“下班通知”

光关掉投递箱还不够!学徒们还在傻傻地盯着任务池,希望有新订单。

站长爷爷修改了学徒们的“工作守则”:

// 旧的守则(死循环)
loop {let job = receiver.lock().unwrap().recv().unwrap(); // 一直等,等不到就死等!job();
}// 新的守则(智能下班)
loop {let message = receiver.lock().unwrap().recv(); // 尝试接收match message {Ok(job) => { // 收到任务,继续工作println!("Worker got a job; executing.");job();}Err(_) => { // 收到错误!说明投递箱关了!println!("Worker disconnected; shutting down.");break; // 聪明地退出循环,准备下班!}}
}
  • 解读
    • 学徒们现在会检查接收结果。
    • 如果 recv() 返回 Err(错误),就说明“投递箱关了,没新任务了”。
    • 这时,他们不再傻等,而是优雅地退出工作循环,准备下班。
第三步:站长爷爷逐个道别

现在,所有学徒都停止了等待,但有些人还在收尾工作(比如把最后一锅汤底加热)。

站长爷爷要做最后一步:

for worker in self.workers.drain(..) {println!("Shutting down worker {}", worker.id);worker.thread.join().unwrap(); // 等待这个学徒完全结束
}
  • 行动
    • 站长爷爷拿出学徒名单(workers)。
    • 一个个叫名字drain(..) 遍历所有学徒)。
    • 对每个学徒说:“辛苦了!等你把手头的事忙完,来我这道个别。”(join()
    • join() 就是等待这个学徒的线程完全结束。
  • 效果
    • 学徒 A 可能还在切肉,站长就耐心等他包装完。
    • 学徒 B 已经没事干了,立刻跑来道别。
    • 站长确保每个学徒都安全、完整地结束了工作,才允许他们离开。

代码优化

use std::io::{BufRead, BufReader, Write};
use std::net::{TcpListener, TcpStream};
use std::sync::{Arc, Mutex, mpsc};
use std::time::Duration;
use std::{fs, thread};struct Worker {id: usize,thread: Option<thread::JoinHandle<()>>,
}pub struct ThreadPool {workers: Vec<Worker>,sender: Option<mpsc::Sender<Job>>,
}type Job = Box<dyn FnOnce() + Send + 'static>;
impl ThreadPool {pub fn new(size: usize) -> ThreadPool {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 {workers.push(Worker::new(id, Arc::clone(&receiver)));}ThreadPool {workers,sender: Some(sender),}}pub fn execute<F>(&self, f: F)whereF: FnOnce() + Send + 'static,{let job = Box::new(f);self.sender.as_ref().unwrap().send(job).unwrap();}
}impl Drop for ThreadPool {fn drop(&mut self) {drop(self.sender.take());for worker in &mut self.workers {println!("Shutting down worker {}", worker.id);if let Some(thread) = worker.thread.take() {thread.join().unwrap();}}}
}impl Worker {fn new(id: usize, receiver: Arc<Mutex<mpsc::Receiver<Job>>>) -> Worker {let thread = thread::spawn(move || {loop {let message = receiver.lock().unwrap().recv();match message {Ok(job) => {println!("Worker {id} got a job; executing.");job();}Err(_) => {println!("Worker {id} disconnected; shutting down.");break;}}}});Worker {id,thread: Some(thread),}}
}fn main() {let listener = match TcpListener::bind("127.0.0.1:7878") {Ok(listener) => listener,Err(e) => {eprintln!("Failed to bind to address: {}", e);return;}};let pool = ThreadPool::new(4);for stream in listener.incoming() {let stream = match stream {Ok(stream) => stream,Err(e) => {eprintln!("Failed to accept connection: {}", e);continue;}};pool.execute(|| {handle_connection(stream);});}
}fn handle_connection(mut stream: TcpStream) {let buf_reader = BufReader::new(&stream);let request_line = match buf_reader.lines().next() {Some(line) => match line {Ok(line) => line,Err(e) => {eprintln!("Failed to read request line: {}", e);send_error_response(stream, "HTTP/1.1 500 INTERNAL SERVER ERROR");return;}},None => {send_error_response(stream, "HTTP/1.1 400 BAD REQUEST");return;}};let (status_line, filename) = match &request_line[..] {"GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"),"GET /sleep HTTP/1.1" => {thread::sleep(Duration::from_secs(5));("HTTP/1.1 200 OK", "hello.html")}_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),};let contents = match fs::read_to_string(filename) {Ok(contents) => contents,Err(e) => {eprintln!("Failed to read file '{}': {}", filename, e);send_error_response(stream, "HTTP/1.1 404 NOT FOUND");return;}};let length = contents.len();let response = format!("{}\r\nContent-Length: {}\r\n\r\n{}",status_line, length, contents);if let Err(e) = stream.write_all(response.as_bytes()) {eprintln!("Failed to write response: {}", e);}
}fn send_error_response(mut stream: TcpStream, status_line: &str) {let contents = match status_line {"HTTP/1.1 404 NOT FOUND" => {"<!DOCTYPE html><html><body><h1>404 Not Found</h1></body></html>"}_ => "<!DOCTYPE html><html><body><h1>Internal Server Error</h1></body></html>",};let length = contents.len();let response = format!("{}\r\nContent-Length: {}\r\n\r\n{}",status_line, length, contents);if let Err(e) = stream.write_all(response.as_bytes()) {eprintln!("Failed to send error response: {}", e);}
}

大结局:一个完美的夜晚

月光节到了,8 点整:

  1. 站长爷爷:收起投递箱,关掉传送阵。
  2. 学徒们:发现没新订单了,停止等待。
  3. 最后一个任务:学徒 3 正在处理一个“超长咒语”(比如 GET /sleep),需要 5 秒。其他学徒已经没事干了。
  4. 站长爷爷:对学徒 0、1、2 说:“你们先去休息吧。”(他们的 join() 很快完成)
  5. 等待:站长耐心等待学徒 3。3 秒… 4 秒… 5 秒!“叮!好了!”
  6. 道别:学徒 3 跑来:“老板,最后一个外卖送出去了!” 站长:“辛苦了,去玩吧!”(join() 完成)
  7. 关门:所有学徒都安全下班,驿站大门轻轻关上,一片祥和。

结局:顾客满意(最后一个请求被处理),学徒开心(安全下班),站长体面(优雅关店)!


总结:优雅关机的三大法宝

  1. 先关“入口”drop(sender),停止接收新任务。
  2. 发“下班信号”:让工作线程检测到 channel 关闭(recv() 返回 Err),主动退出循环。
  3. 逐个“道别”:主线程用 join() 等待每个工作线程完成最后的工作,确保资源安全释放。

(全剧终)


文章转载自:

http://Bl49hGBk.zddbz.cn
http://u8l54x8L.zddbz.cn
http://HIuzMJ8U.zddbz.cn
http://yv9CBlyN.zddbz.cn
http://OgVAI2Sl.zddbz.cn
http://CmtSqsBf.zddbz.cn
http://vrLEWcw5.zddbz.cn
http://JJ96uNaf.zddbz.cn
http://eW0QWfMk.zddbz.cn
http://RFTR6zxi.zddbz.cn
http://N7PUW8tn.zddbz.cn
http://AiTlg2Hp.zddbz.cn
http://jLEtBevS.zddbz.cn
http://1WgN1nHU.zddbz.cn
http://kbXMDFMf.zddbz.cn
http://I2yiIkx5.zddbz.cn
http://7C6Re6Ew.zddbz.cn
http://nD5GOJJ9.zddbz.cn
http://r53fnpNM.zddbz.cn
http://7nxpwODx.zddbz.cn
http://OWIcWQm7.zddbz.cn
http://4UWh7HSd.zddbz.cn
http://b92wzEcq.zddbz.cn
http://G7HeXDbG.zddbz.cn
http://VNvvJD00.zddbz.cn
http://EwjFD3qY.zddbz.cn
http://6JssGPqC.zddbz.cn
http://HjK1Wylb.zddbz.cn
http://oaKJgz4y.zddbz.cn
http://94C3BKtH.zddbz.cn
http://www.dtcms.com/a/386372.html

相关文章:

  • 机器人导论 第六章 动力学(1)——牛顿欧拉法推导与详述
  • Android U 浮窗——整体流程介绍(更新中)
  • Pytest+request+Allure
  • Android 反调试攻防实战:多重检测手段解析与内核级绕过方案
  • [vue.js] 树形结点多选框选择
  • websocket python 实现
  • 使用代理访问网络各项命令总结
  • 信创电脑入门指南:定义、发展历程与重点行业部署详解
  • PostgreSQL——元命令
  • PHP 连接池详解:概念、实现与最佳实践
  • nginx + php-fpm改用socket方式代理可能遇到的问题
  • 一篇文章说清【布隆过滤器】
  • 「数据获取」《中国教育经费统计年鉴》(1997-2024)
  • 产品开发周期缩写意思
  • Keil5安装教程保姆级(同时兼容支持C51与ARM双平台开发)(附安装包)
  • [deepseek]Python文件打包成exe指南
  • 2025最新超详细FreeRTOS入门教程:第二十章 FreeRTOS源码阅读与内核解析
  • 一种基于最新YOLO系列优化策略的缺陷检测方法及系统
  • 「英」精益设计第二版 — AxureMost落葵网
  • esp32_rust_oled
  • 贪心算法应用:前向特征选择问题详解
  • 微信小程序禁止下拉
  • 概率思维:数据驱动时代的核心技术引擎与方法论修炼
  • Docker在欧拉系统上内核参数优化实践
  • 【Linux系列】查询磁盘类型
  • 机械革命笔记本电脑重装Windows系统详细教程
  • RustFS vs MinIO:深入对比分布式存储的性能、功能与选型指南
  • GLSL 版本与应用场景详解
  • QNX与Linux的详细对比分析
  • PHP 并发处理与进程间通信深度解析