说句题外话,这篇文章非常要求Rust的各方面知识,最好看一下我的【Rust自学】专栏的所有内容。这篇文章也是整个专栏最长(4762字)的文章,需要多次阅读消化,最好点个收藏,免得刷不到了。
喜欢的话别忘了点赞、收藏加关注哦,对接下来的教程有兴趣的可以关注专栏。谢谢喵!(=・ω・=)
20.2.1. 回顾
我们在上一篇文章中写了一个简单的本地服务器,但是这个服务器是单线的,也就是说请求一个一个进去之后我们得一个一个地处理,如果某个请求处理得慢,那后面的都得排队等着。这种单线程外部服务器的性能是非常差的。
20.2.2. 慢速请求
我们用代码来模拟慢速请求:
use std::{
fs,
io::{
prelude::*, BufReader},
net::{
TcpListener, TcpStream},
thread,
time::Duration,
};
// ...
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));
("HTTP/1.1 200 OK", "hello.html")
}
_ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
};
// ...
}
省略了一些原代码,但是不影响。我们增加的语句是如果用户访问的是127.0.0.1:7878/sleep
时会调用thread::sleep(Duration::from_secs(5));
,这句话使代码的执行休眠5秒,也就是模拟的慢速请求。
然后打开两个浏览器窗口:一个用于http://127.0.0.1:7878/另一个为http://127.0.0.1:7878/sleep。如果像以前一样,您会看到它快速响应。但是如果你输入/sleep
然后加载 ,你会看到一直等到 sleep
在加载前已经休眠了整整5秒。
如何改善这种情况呢?这里我们使用线程池技术,也可以选择其它技术比如fork/join模型、 单线程异步 I/O 模型或多线程异步I/O模型。
20.2.3. 使用线程池提高吞吐量
线程池是一组分配出来的线程,它们被用于等待并随时可能的任务。当程序接收到一个新任务时,它会给线程池里边一个线程分配这个任务,其余线程与此同时还可以接收其它任务。当任务执行完后,这个线程就会被重新放回线程池。
线程池通过允许并发处理连接的方式增加了服务器的吞吐量。
如何为每个连接都创建一个线程呢?看代码:
fn main() {
let listener = TcpListener::bind("127.0.0.1:7878").unwrap();
for stream in listener.incoming() {
let stream = stream.unwrap();
thread::spawn(|| {
handle_connection(stream);
});
}
}
迭代器每迭代一次就创建一个新线程来处理。
这样写的缺点在于线程数量没有限制,每一个请求就创建一个新线程。如果黑客使用DoS(Denial of Service,拒绝服务攻击),我们的服务器就会很快瘫掉。
所以在上边代码的基础上我们进行修改,我们使用编译驱动开发编写代码(不是一个标准的开发方法论,是开发者之间的一种戏称,不同于TDD测试驱动开发):把期望调用的函数或是类型写上,再根据编译器的错误一步步修改。
使用编译驱动开发
我们把我们想写的代码直接写上,先不论对错:
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);
})
}
}
虽然说并没有ThreadPool
这个类型,但是根据编译驱动开发编写代码的逻辑,我觉得应该这么写就先写上,不管对错。
使用cargo check
检查一下:
error[E0433]: failed to resolve: use of undeclared type `ThreadPool`
--> src/main.rs:11:16
|
9 | let pool = ThreadPool::new(4);
| ^^^^^^^^^^ use of undeclared type `ThreadPool`
For more information about this error, try `rustc --explain E0433`.
error: could not compile `hello` (bin "hello") due to 1 previous error
这个错误告诉我们我们需要一个ThreadPool
类型或模块,所以我们现在就构建一个。
我们在lib.rs
中写ThreadPool
的相关代码,一方面保持了main.rs
足够简洁,另一方面也使ThreadPool
相关代码能更加独立地存在。
打开lib.rs
,写下ThreadPool
的简单定义:
pub struct ThreadPool;
在main.rs
里把ThreadPool
引入作用域:
use web_server::ThreadPool;
使用cargo check
检查一下:
error[E0599]: no function or associated item named `new` found for struct `ThreadPool` in the current scope
--> src/main.rs:10:28
|
10 | let pool = ThreadPool::new(4);
| ^^^ function or associated item not found in `ThreadPool`
这个错误表明接下来我们需要创建一个名为的关联函数 ThreadPool
的new
。我们还知道new
需要有一个参数,该参数可以接受4
作为参数,并且应该返回一个ThreadPool
实例。让我们实现具有这些特征的最简单的new
函数:
pub struct ThreadPool;
impl ThreadPool {
pub fn new(size: usize) -> ThreadPool {
ThreadPool
}
}
使用cargo check
检查一下:
error[E0599]: no method named `execute` found for struct `ThreadPool` in the current scope
--> src/main.rs:17:14
|
15 | pool.execute(|| {
| -----^^^^^^^ method not found in `ThreadPool`
For more information about this error, try `rustc --explain E0599`.
error: could not compile `hello` (bin "hello") due to 1 previous error
现在发生错误是因为我们在ThreadPool
上没有execute
方法。那就补充一个方法:
pub fn execute<F>(&self, f: F)
where
F: FnOnce() + Send + 'static,
{
}
-
execute
函数的参数除了self
的应用还有一个闭包参数,运行请求的线程只会调用闭包一次,所以使用FnOnce()
,()
表示它是返回单位类型()
的闭包。同时我们需要Send
trait将闭包从一个线程传输到另一个线程,而'static
是因为我们不知道线程执行需要多长时间。 -
也可以这么想:我们使用它替代的是原代码的
thread::spawn
函数,所以修改时就可以借鉴它的函数签名,它的签名如下。我们主要借鉴的是泛型F
和它的约束,所以excute
函数的泛型约束就可以按照F
来写。
pub fn spawn<F, T>(f: F) -> JoinHandle<