【Rust自学】20.2. 最后的项目:多线程Web服务器

说句题外话,这篇文章非常要求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`

这个错误表明接下来我们需要创建一个名为的关联函数 ThreadPoolnew 。我们还知道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<
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值