使用rust写一个Web服务器——async-std版本

文章目录

使用rust实现一个异步运行时是async-std的单线程Web服务器。

仓库地址: 1037827920/web-server: 使用rust编写的简单web服务器 (github.com)

在之前的单线程版本的Web服务器代码上进行修改,具体代码在给的仓库地址中。

实现异步代码

首先将handle_connection修改为async实现:

rust 复制代码
async fn handle_connection(mut stream: TcpStream) {}

该修改会将函数的返回值从()变成Future<Output = ()>,因此直接运行将不再有任何效果,只用通过.await或执行器的poll。

使用async-std作为异步运行时:

async-std运行时允许使用属性#[async_std::main]将我们的fn main函数变成async fn main,这样就可以在main函数中直接调用其他async函数,否则你得用block_on方法来让main去阻塞等待异步函数的完成,但是这种简单粗暴的阻塞等待方式并不灵活

Cargo.toml:

toml 复制代码
[dependencies]
futures = "0.3"

[dependencies.async-std]
version = "1.6"
features = ["attributes"]

下面将main函数修改为异步的,并在其中调用前面修改的异步版本handle_connection:

rust 复制代码
use std::{
    io::{prelude::*, BufReader},
    net::{TcpListener, TcpStream},
    fs,
    time::Duration,
};
extern crate async_std;
use async_std::task;

#[async_std::main]
async fn main() {
    let listener = TcpListener::bind("localhost:8080").unwrap();
    for stream in listener.incoming() {
        let stream = stream.unwrap();
        // 这里还是无法并发
        handle_connection(stream).await;
    }
}

实现异步版本的handle_connection:

rust 复制代码
/// # 函数作用
/// 处理连接:读取请求,回应请求
async fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    // 使用next而不是lines,因为我们只需要读取第一行,判断具体的request方法
    let request_line = buf_reader.lines().next().unwrap().unwrap();

    // 根据请求的不同,返回不同的响应
    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), // 请求 / 资源
        "GET /sleep HTTP/1.1" => { // 请求 /sleep 资源
            // 没有使用std::thread::sleep进行睡眠,原因是该函数是阻塞的,它会让当前线程陷入睡眠中,导致其他任务无法继续运行
            task::sleep(Duration::from_secs(5)).await;
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();
    
    let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    // write_all接收&[u8]类型作为参数,这里需要用as_bytes将字符串转换为字节数组
    stream.write_all(response.as_bytes()).unwrap();
}

可以看出,只是把函数变成async往往是不够的,还需要将它内部的代码也都变成异步兼容,阻塞线程绝对是不可行的

但是线程web服务器还是不能进行并发处理请求,原因是listener.incoming()是阻塞的迭代器。当listener在等待连接时,执行器是无法执行其他Future的,而且只有当我们处理完已有的连接后,才能接收新的连接。

并发地处理连接

上面的解决方法是将listener.incoming()从一个阻塞的迭代器变成一个非阻塞的Stream

rust 复制代码
use std::{
    fs,
    time::Duration,
};
extern crate async_std;
use async_std::{
    net::{TcpListener, TcpStream},
    io::{prelude::*, BufReader},
    task,
};
use futures::StreamExt;

#[async_std::main]
async fn main() {
    let listener = TcpListener::bind("localhost:8080").await.unwrap();
    
    listener
        .incoming()
        .for_each_concurrent(None, |tcpstream| async move {
            let tpcstream = tcpstream.unwrap();
            handle_connection(tpcstream).await;
        })
        .await;
}

异步版本的TcpListener为listener.incoming()实现了Stream trait,这样listener.incoming()不再阻塞,且使用for_each_concurrent可以并发地处理从Stream获取的元素。

现在关键在于handle_connection不能再阻塞:

rust 复制代码
/// # 函数作用
/// 处理连接:读取请求,回应请求
async fn handle_connection(mut stream: TcpStream) {
    let buf_reader = BufReader::new(&mut stream);
    // 使用next而不是lines,因为我们只需要读取第一行,判断具体的request方法
    let request_line = buf_reader.lines().next().await.unwrap().unwrap();

    // 根据请求的不同,返回不同的响应
    let (status_line, filename) = match &request_line[..] {
        "GET / HTTP/1.1" => ("HTTP/1.1 200 OK", "hello.html"), // 请求 / 资源
        "GET /sleep HTTP/1.1" => { // 请求 /sleep 资源
            // 没有使用std::thread::sleep进行睡眠,原因是该函数是阻塞的,它会让当前线程陷入睡眠中,导致其他任务无法继续运行
            task::sleep(Duration::from_secs(5)).await;
            ("HTTP/1.1 200 OK", "hello.html")
        }
        _ => ("HTTP/1.1 404 NOT FOUND", "404.html"),
    };

    let contents = fs::read_to_string(filename).unwrap();
    let length = contents.len();
    
    let response = format!("{status_line}\r\nContent-Length: {length}\r\n\r\n{contents}");

    // write_all接收&[u8]类型作为参数,这里需要用as_bytes将字符串转换为字节数组
    stream.write_all(response.as_bytes()).await.unwrap();
}

在将数据读写改造成异步后,现在该函数也彻底变成了异步版本,可以并发地处理连接

使用多线程提升性能

async并发和多线程其实并不冲突,async-std包也允许我们使用多个线程去处理,由于handle_connection实现了Send trait不会阻塞,因此使用async_std::task::spawn是非常安全的:

rust 复制代码
use async_std::task::spawn;

#[async_std::main]
async fn main() {
    let listener = TcpListener::bind("localhost:8080").await.unwarp():
    listener
    	.incoming()
    	.for_each_concurrent(None, |stream| async move {
            let stream = stream.unwrap();
            spawn(handle_connection(stream));
    })
    .await;
}

但是这里是为每个请求都单独创建了一个线程,实际上需要限制创建线程的数量,可以通过线程池来实现。具体可以看这篇无async的多线程版本的Web服务器

相关推荐
崔庆才丨静觅7 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60618 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅8 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
七夜zippoe8 小时前
CANN Runtime任务描述序列化与持久化源码深度解码
大数据·运维·服务器·cann
盟接之桥8 小时前
盟接之桥说制造:引流品 × 利润品,全球电商平台高效产品组合策略(供讨论)
大数据·linux·服务器·网络·人工智能·制造
崔庆才丨静觅9 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅9 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端