1.6 综合篇

2022-11-01 • 更新于 2023-10-29

开始

在工作空间的 Cargo.toml 里添加 "ch01/basics",添加之后内容如下所示:

[workspace]

members = [
    "ch01/helloworld",
    "ch01/error-handling",
    "ch01/state",
    "ch01/nested-routing",
    "ch01/static-files",
    "ch01/basics"
]

然后在文件夹 ch01 下创建 basics 项目,并和之前一样添加好依赖、编写好 main.rs 的内容。

(项目的 Cargo.toml)

[dependencies]
actix-web = "4.2.1"

(main.rs)

use actix_web::{HttpServer, App};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

这一次,我们将官方源码中的 static 下的内容直接复制到项目下。有两个 Html、一张图片和一个图标文件。Html 的内容也不复杂,welcome.html 显示了一行字和一个图片,404.html 中则是一个超链接,另一个是一行文字。

在官方例子的 Readme.md 中,列出了访问路径和会出现的结果。那么这些也将是我们之后需要做的。

添加中间件

中间件先从最熟悉的 Logger 开始添加。

在 Cargo.toml 中添加对 env_loggerlog 的依赖:

env_logger = "0.9"
log = "0.4"

然后在主函数 HttpServer 初始化前,初始化好 env_logger,并使用 log::info!() 打印一段消息,表示程序启动。

#[actix_web::main]
async fn main() -> std::io::Result<()> {

    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(...)
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

之后在 App 中,添加 Logger 中间件。

use actix_web::{HttpServer, App, middleware};

#[actix_web::main]
async fn main() -> std::io::Result<()> {

    env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));

    log::info!("starting HTTP server at http://localhost:8080");

    HttpServer::new(|| {
        App::new()
            // add here
            .wrap(middleware::Logger::default())
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

接下来添加 Compress 中间件,暂时不清楚用处。值得注意的是 Compress 中间件需要优先注册,所以将其放在 Logger 前:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // log...
    HttpServer::new(|| {
        App::new()
            // add here
            .wrap(middleware::Compress::default())
            .wrap(middleware::Logger::default())
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

之后添加 SessionMiddleware 中间件,但是需要提前添加引用:

actix-session = { version = "0.7", features = ["cookie-session"] }

查看文档,注意到两条比较重要的信息,即 SessionMiddleware 主要的工作:

  • 根据 Session 的状态和已经执行的操作,来指示 Session 存储后端为该 Session 附加上创建/更新/删除/检索状态。(按我的理解,就是可以对 Session 执行四种操作)
  • 在客户端,设置/删除 Cookie,确保用户在发送不同的 Http 请求时, 始终关联同一个 Session。

SessionMiddleware 的添加比其他两个中间件复杂一点。首先需要创建一个数组 SESSION_SIGNING_KEY,并通过这个数组生成一个 key,然后在 SessionMiddleware 构建的时候传入。官方源码中,还设置了 cookie_sercue,大致是用来确保 http 也能够传输:

use actix_session::{SessionMiddleware, storage::CookieSessionStore};
use actix_web::{HttpServer, App, middleware};

static SESSION_SIGNING_KEY: &[u8] = &[0; 64];

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // log...

    let key = actix_web::cookie::Key::from(SESSION_SIGNING_KEY);

    HttpServer::new(move || {
        App::new()
            .wrap(middleware::Compress::default())
            // add here
            .wrap(
                SessionMiddleware::builder(CookieSessionStore::default(), key.clone())
                    .cookie_secure(false)
                    .build()
            )
            .wrap(middleware::Logger::default())
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

路由配置

一共是 9 个路由,按官方源码来一个一个配置。(注意不是之前 Readme.md 中的顺序)

favicon 路由配置

favicon 函数对应的路径是 "/favicon",只做一件事,就是返回 favicon.ico 文件。在此之前需要先添加对 actix-file 的依赖:

actix-files = "0.6"

添加 favicon 函数,直接使用 actix_web::get 宏配置路径。注意到这时使用的 Resultactix_web::Result

use actix_files::NamedFile;
use actix_session::{SessionMiddleware, storage::CookieSessionStore};
use actix_web::{HttpServer, App, middleware, Responder, get, Result};

#[get("/favicon")]
async fn favicon() -> Result<impl Responder> {
    Ok(NamedFile::open("static/favicon.ico")?)
}

在主函数中,通过 service 函数,将 favicon 路由添加到 App 中:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ....

    HttpServer::new(move || {
        App::new()
            // middle ware register
            // add favicon here
            .service(favicon)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

可以尝试运行并访问 http://localhost:8080/favicon,没出错应该能够看到齿轮图标。

welcome 路由

welcome 路由的路径也是直接通过 actix_web::get 宏来配置,指定的路径为 "/welcome"welcome 函数传入的参数类型为 HttpRequestSession,两种类型都实现了 FromRequest trait。官方源码中,首先打印了这一次请求的内容:

use actix_session::{SessionMiddleware, storage::CookieSessionStore, Session};
use actix_web::{HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result};

#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
    print!("{req:?}");
    todo!()
}

然后,为 session 添加了一个计数器,在每次访问 "/welcome" 加一。设置计数器的值的方式是,先新建一个 counter 整型可变变量,初始值为 1,然后获取 session 中的 counter,并提取到 count 变量中。如果存在则将 counter 的值设置为 count 加一,否则则会将 counter 的初始值设置给 session 中的counter。这样的话,在第一次访问时,因为 session 中没有 counter,所以在第一次访问之后,session 中新增了 counter,并且值为 1。在之后访问时,就可以获取到 counter 的值。

#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
    print!("{req:?}");
    // set counter for session
    let mut counter = 1;

    if let Some(count) = session.get::<i32>("counter")? {
        counter = count + 1;
    }

    session.insert("counter", counter)?;

    todo!()
}

接下来是写好返回值。返回的状态码为 200,内容类型为 plaintext,内容直接为 welcome.html 中的所有内容。

use actix_web::{
    HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result, 
    http::{
        StatusCode, header::ContentType
    }
};

#[get("/welcome")]
async fn welcome(req: HttpRequest, session: Session) -> Result<HttpResponse> {
    print!("{req:?}");
    // set counter for session
    let mut counter = 1;

    if let Some(count) = session.get::<i32>("counter")? {
        counter = count + 1;
    }

    session.insert("counter", counter)?;

    Ok(HttpResponse::build(StatusCode::OK)
        .content_type(ContentType::plaintext())
        .body(include_str!("../static/welcome.html")))
}

最后将 welcome 添加到 App 中。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ...
    HttpServer::new(move || {
        App::new()
            // middle ware register
            .service(favicon)
            // add here
            .service(welcome)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

可以尝试运行并访问 http://localhost:8080/welcome。没出错应该会看到 welcome.html 的源码。可以尝试将 .content_type(ContentType::plaintext()) 改为 .content_type(ContentType::html()),不过图片会无法访问,所以无法显示。

路径 "/user/{name}" 的路由

路径对应的路由为 with_param,表示访问时带有参数。应该是匹配 {name} 的内容,使用提取器来获取。提取器的位置在函数的传入参数中,这里使用到了 Path 提取器。

use actix_web::web;

async fn with_param(req: HttpRequest, path: web::Path<String>) -> HttpResponse {
    print!("{req:?}");
    todo!()
}

返回的状态码为 200,内容类型为 plaintext,返回的内容为一串字符串,不过字符串中会出现路径中 {name} 处的字符串。

async fn with_param(req: HttpRequest, path: web::Path<String>) -> HttpResponse {
    print!("{req:?}");
    
    HttpResponse::Ok()
        .content_type(ContentType::plaintext())
        .body(format!("Hello {}!", path))
}

最后在 Appservice 配置路径和路由:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ....
    HttpServer::new(move || {
        App::new()
            // middel ware
            .service(favicon)
            .service(welcome)
            // add here
            .service(web::resource("/user/{name}").route(web::get().to(with_param)))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

如果访问 http://localhost:8080/user/bob,可以看到网页上显示的字符串为 Hello bob!,当然如果 bob 换成其他字符串也能够显示。

在传入的参数中,我没有像官方一样使用 web::Path<(String,)>,指定元组,而是直接指定了 String。当然也是能够运行的。另外,也可以在函数上直接使用 #[get("/user/{name}")] 配置路由的路径,这样一来,App 这边应该改为 App.service(with_param)

路径 "/async-body/{name}" 的路由

路径 "/async-body/{name}" 对应的路由函数为 response_body。传入的参数为路径提取器,提取类型为 String

async fn response_body(path: web::Path<String>) -> HttpResponse {
    todo!()
}

之后从提取器中获取提取的值,在返回 Http 响应时,返回了响应流。响应流通过 stream! 创建了迭代器,模拟实现异步操作。返回的内容为 Hello {name}!,分单词符号异步传输。

首先需要添加 async-stream 依赖。

async-stream = "0.3"

然后编写 response_body 函数:

use std::convert::Infallible;
use async_stream::stream;

async fn response_body(path: web::Path<String>) -> HttpResponse {
    let name = path.into_inner();

    HttpResponse::Ok().streaming(stream! {
        yield Ok::<_, Infallible>(web::Bytes::from("Hello "));
        yield Ok::<_, Infallible>(web::Bytes::from(name));
        yield Ok::<_, Infallible>(web::Bytes::from("!"));
    })
}

yield Ok::<T,E> 中,_ 代表自动识别类型,后面传入的是 web::Bytes,所以这里也是这个类型。而 E 指定的类型为 Infallible,查看文档,发现这个类型代表着 Result 不会返回错误。

编写好后,将 response_body 添加到 App 里。当然,路径可以通过 #[get()] 宏直接配置,也可以通过 web::resource().to()web::resource().route(web::get().to()) 配置。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ....
    HttpServer::new(move || {
        App::new()
            // middle ware
            .service(favicon)
            .service(welcome)
            .service(with_param)
            // add here
            .service(web::resource("/async-body/{name}").route(web::get().to(response_body)))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

运行并访问 http://localhost:8080/async-body/bob,应该会出现 Hello bob!

路径 /test 的路由

/test 的路由采用了匿名函数的形式,直接在配置时添加。在访问这个路径时,如果请求的方法为 GET,则会返回 200;如果为 POST,则会返回 405;其他的请求方法,则会返回 404。

use actix_web::{
    HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result, 
    http::{
        StatusCode, header::ContentType, Method
    }, web
};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ....
    HttpServer::new(move || {
        App::new()
            // middle ware
            // other path and route
            // path "/test"
            .service(
                web::resource("/test").to(|req: HttpRequest| match *req.method() {
                    Method::GET => HttpResponse::Ok(),
                    Method::POST => HttpResponse::MethodNotAllowed(),
                    _ => HttpResponse::NotFound(),
                })
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

可以使用一些工具来对 http://localhost:8080/test 进行测试。

路径 /error 的路由

/test 的路由一样是直接使用匿名函数来配置。函数内就构建了一个 InternalError 并返回。

use std::{convert::Infallible, io};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ....
    HttpServer::new(move || {
        App::new()
            // other configuration
            // path "/error"
            .service(
                web::resource("/error").to(|| async {
                    actix_web::error::InternalError::new(
                        io::Error::new(io::ErrorKind::Other, "test"),
                        StatusCode::INTERNAL_SERVER_ERROR,
                    )
                })
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

编写完代码后,运行并访问 http://localhost:8080/error,可以看到 test,和返回值中创建并传入的 io::Error 中的字符串一致。而控制台中,会得到一行状态码为 500 的输出。

路径 /static 的路由

在访问路径 /static 时,会直接以文件形式显示目录 static 下的所有内容。

use actix_files::{NamedFile, Files};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ....
    HttpServer::new(move || {
        App::new()
            // other configuration
            // path "/static"
            .service(Files::new("/static", "static").show_files_listing())
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

运行并访问 http://localhost:8080/static,应该可以看到 static 目录下的所有文件,这些文件都是可以访问的。

根路径的路由

在访问根路径时,会重定向到 static/welcome.html。

use actix_web::{
    HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result, 
    http::{
        StatusCode, header::{ContentType, self}, Method
    }, web
};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // ...
    HttpServer::new(move || {
        App::new()
            // other configuration
            // path "/"
            .service(
                web::resource("/").route(web::get().to(|req: HttpRequest| async move{
                    println!("{req:?}");

                    HttpResponse::Found()
                        .insert_header((header::LOCATION, "static/welcome.html"))
                        .finish()
                }))
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

insert_header 中,传入的 header::LOCATION 代表重定向。如果运行并访问 http://localhost:8080/,会发现链接变成了 http://localhost:8080/static/welcome.html,并且显示了 welcome.html 的网页内容。

默认路由配置

默认路由是在访问其他路径时,如果没有对应路由,则会运行默认路由。

默认路由对应的函数为 default_handler。函数内,逻辑大概是,如果请求方法为 GET,则返回 404.html 网页;否则返回 405 状态码:

use actix_web::{
    HttpServer, App, middleware, Responder, get, HttpRequest, HttpResponse, Result, 
    http::{
        StatusCode, header::{ContentType, self}, Method
    }, web, Either
};

async fn default_handler(req_method: Method) -> Result<impl Responder> {
    match req_method {
        Method::GET => {
            let file = NamedFile::open("static/404.html")?
                .customize()
                .with_status(StatusCode::NOT_FOUND);
            Ok(Either::Left(file))
        },
        _ => Ok(Either::Right(HttpResponse::MethodNotAllowed().finish()))
    }
}

Either 的作用大概是将两种不同的类型塞到同一个类型中。

编写好后访问没有配置过路由的路径,会显示 404 的网页。

总结

列一下这次出现的一些东西:

  1. 路由配置

    • 直接使用宏配置,如 #[get()]
    • App::new().service(web::resouce().to()),即调用 Appservice 函数配置,路径使用 web::resouce配置,路由通过链式调用 .to()配置。
    • App::new().service(web::resouce().route(web::{method}().to())){method} 带指请求方法。
    • 静态文件路由,Files::new("{网站路径}", {文件路径}),单个文件调用 index_file,要显示列表调用 show_files_listing
    • 在路由中返回静态文件,NamedFile::open
  2. Session的使用:

    Cookie 的创建:首先创建/生成/读取一个 64 比特的字符串,即 &[u8] 数组,长度为 64。然后通过 actix_web::cookie::Key::from 创建 Key。之后在 App 中添加中间件 SessionMiddleware 时,通过 SessionMiddleware::builder 传入 Key

    Cookie 的使用:使用时,在路由函数的传入参数中,指定一个类型为 Session 的变量,通过调用这个变量的 get::<T> 来获取 cookie,也可以通过 insert 插入一个 key/value 对。

actix-web学习笔记Rustactix-web学习笔记

学少何

不求上进的社畜……

1.5 静态文件