【跟小嘉学 Rust 编程】三十四、Rust的Web开发框架之一: Actix-Web的进阶

系列文章目录

【跟小嘉学 Rust 编程】一、Rust 编程基础
【跟小嘉学 Rust 编程】二、Rust 包管理工具使用
【跟小嘉学 Rust 编程】三、Rust 的基本程序概念
【跟小嘉学 Rust 编程】四、理解 Rust 的所有权概念
【跟小嘉学 Rust 编程】五、使用结构体关联结构化数据
【跟小嘉学 Rust 编程】六、枚举和模式匹配
【跟小嘉学 Rust 编程】七、使用包(Packages)、单元包(Crates)和模块(Module)来管理项目
【跟小嘉学 Rust 编程】八、常见的集合
【跟小嘉学 Rust 编程】九、错误处理(Error Handling)
【跟小嘉学 Rust 编程】十一、编写自动化测试
【跟小嘉学 Rust 编程】十二、构建一个命令行程序
【跟小嘉学 Rust 编程】十三、函数式语言特性:迭代器和闭包
【跟小嘉学 Rust 编程】十四、关于 Cargo 和 Crates.io
【跟小嘉学 Rust 编程】十五、智能指针(Smart Point)
【跟小嘉学 Rust 编程】十六、无畏并发(Fearless Concurrency)
【跟小嘉学 Rust 编程】十七、面向对象语言特性
【跟小嘉学 Rust 编程】十八、模式匹配(Patterns and Matching)
【跟小嘉学 Rust 编程】十九、高级特性
【跟小嘉学 Rust 编程】二十、进阶扩展
【跟小嘉学 Rust 编程】二十一、网络编程
【跟小嘉学 Rust 编程】二十三、Cargo 使用指南
【跟小嘉学 Rust 编程】二十四、内联汇编(inline assembly)
【跟小嘉学 Rust 编程】二十五、Rust命令行参数解析库(clap)
【跟小嘉学 Rust 编程】二十六、Rust的序列化解决方案(Serde)
【跟小嘉学 Rust 编程】二十七、Rust 异步编程(Asynchronous Programming)
【跟小嘉学 Rust 编程】二十八、Rust中的日期与时间
【跟小嘉学 Rust 编程】二十九、Rust 中的零拷贝序列化解决方案(rkyv)
【跟小嘉学 Rust 编程】三十、Rust 使用 Slint UI
【跟小嘉学 Rust 编程】三十一、Rust的日志与追踪
【跟小嘉学 Rust 编程】三十二、Rust的设计模式(Design Patterns)
【跟小嘉学 Rust 编程】三十三、Rust的Web开发框架之一: Actix-Web的基础
【跟小嘉学 Rust 编程】三十四、Rust的Web开发框架之一: Actix-Web的进阶

文章目录

  • 系列文章目录
    • @[TOC](文章目录)
  • 前言
  • 一、Actix-Web 进阶
    • 1.1、错误
      • 1.1.1、错误介绍
      • 1.1.2、自定义错误响应
      • 1.1.3、错误辅助
      • 1.1.4、错误日志
      • 1.1.5、错误处理的推荐方式
      • 1.1.6、日志记录
    • 1.2、URL分发
      • 1.2.1、URL分发介绍
      • 1.2.2、资源配置
        • 1.2.2.1、配置路由
      • 1.2.3、路由匹配
      • 1.2.4、资源匹配语法
      • 1.2.5、路由范围
      • 1.2.6、匹配信息
        • 1.2.6.1、路径信息提取器
      • 1.2.7、生成资源URL
      • 1.2.8、外部资源
      • 1.2.9、路径规范化和重定向到斜杠附加到路由
        • 1.2.9.1、使用应用程序前缀组合应用程序
      • 1.2.10、自定义路由守卫
        • 1.2.10.1、修改守卫的值
      • 1.2.11、修改默认 NOT Found 响应
    • 1.3、请求
      • 1.3.1、JSON 请求
        • 1.3.1.1、`JSON` 提取器
        • 1.3.1.2、Payload 提取
      • 1.3.2、Content 编码
      • 1.3.3、分块传输编码(Chunked transfer encoding)
      • 1.3.4、文件上传
      • 1.3.5、URL编码的内容
      • 1.3.6、流式请求
    • 1.4、响应
      • 1.4.1、JSON 响应
      • 1.4.2、Content 编码
    • 1.5、测试
      • 1.5.1、集成测试
      • 1.5.2、流响应测试
      • 1.5.3、单元测试
    • 1.6、中间件(Middleware)
      • 1.6.1、中间件介绍
      • 1.6.2、日志中间件
        • 1.6.2.1、使用
        • 1.6.2.2、格式
      • 1.6.3、Default headers
      • 1.6.4、用户会话
      • 1.6.5、错误处理
    • 1.7、静态文件
      • 1.7.1、单个文件
      • 1.7.2、目录
      • 1.7.3、配置
  • 二、WebSocket
  • 三、热加载(Auto-Reloading)
  • 四、Shuttle
  • 总结

前言

上一章,我们讲解了 Actix Web的基础,现在讲解 Actix web的进阶,包含了错误处理、URL 分发原理、请求响应处理、中间件、静态文件、Websocket集成、热加载以及 shuttle 部署。

主要教材参考 《The Rust Programming Language》
主要教材参考 《Rust For Rustaceans》
主要教材参考 《The Rustonomicon》
主要教材参考 《Rust 高级编程》
主要教材参考 《Cargo 指南》
主要教材参考 《Rust 异步编程》
主要教材参考 《Rust 设计模式》


一、Actix-Web 进阶

1.1、错误

1.1.1、错误介绍

Actix Web 使用自己的 actix_web::error::Error 类型 和actix_web::error::ResponseError 特征来进行错误处理。如果一个处理器返回了一个 Error,同样实现了 ResponseError 的Reulst,Actix Web 会把该错误呈现为带有 actix_web:: HTTP::StatusCode 的HTTP响应

pub trait ResponseError {
    fn error_response(&self) -> Response<Body>;
    fn status_code(&self) -> StatusCode;
}

1.1.2、自定义错误响应

use actix_web::{error, Result};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
#[display(fmt = "my error: {}", name)]
struct MyError {
    name: &'static str,
}

// Use default implementation for `error_response()` method
impl error::ResponseError for MyError {}

async fn index() -> Result<&'static str, MyError> {
    Err(MyError { name: "test" })
}

ResponseError 有默认的 error_response 实现,它讲渲染为500 错误, 这是上述 index 处理器将会发生的。我们也可以覆盖 error_response 方法。

use actix_web::{
    error, get,
    http::{header::ContentType, StatusCode},
    App, HttpResponse,
};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum MyError {
    #[display(fmt = "internal error")]
    InternalError,

    #[display(fmt = "bad request")]
    BadClientData,

    #[display(fmt = "timeout")]
    Timeout,
}

impl error::ResponseError for MyError {
    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .insert_header(ContentType::html())
            .body(self.to_string())
    }

    fn status_code(&self) -> StatusCode {
        match *self {
            MyError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
            MyError::BadClientData => StatusCode::BAD_REQUEST,
            MyError::Timeout => StatusCode::GATEWAY_TIMEOUT,
        }
    }
}

#[get("/")]
async fn index() -> Result<&'static str, MyError> {
    Err(MyError::BadClientData)
}

1.1.3、错误辅助

Actix Web 提供了一组错误辅助函数,用于从其他错误生成特定的HTTP 错误代码。这里我们使用 map_err 将 MyError 转换为 400(错误请求)。

use actix_web::{error, get, App, HttpServer};

#[derive(Debug)]
struct MyError {
    name: &'static str,
}

#[get("/")]
async fn index() -> actix_web::Result<String> {
    let result = Err(MyError { name: "test error" });

    result.map_err(|err| error::ErrorBadRequest(err.name))
}

1.1.4、错误日志

Actix 日志所有错误都在 WARN 日志级别。如果应用日志级别设置为 DEBUG,并且开启 RUST_BACKTRACE,错误的堆栈信息都会记录。

RUST_BACKTRACE=1 RUST_LOG=actix_web=debug cargo run

1.1.5、错误处理的推荐方式

考虑将应用程序产生的错误分为两大类:面向用户的错误和非面向用户的错误。

use actix_web::{
    error, get,
    http::{header::ContentType, StatusCode},
    App, HttpResponse, HttpServer,
};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum UserError {
    #[display(fmt = "Validation error on field: {}", field)]
    ValidationError { field: String },
}

impl error::ResponseError for UserError {
    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .insert_header(ContentType::html())
            .body(self.to_string())
    }
    fn status_code(&self) -> StatusCode {
        match *self {
            UserError::ValidationError { .. } => StatusCode::BAD_REQUEST,
        }
    }
}

并不是所有的错误都需要返回错误信息,例如服务器发生了故障,我们需要对用户隐藏细节,例如数据库宕机等等。

use actix_web::{
    error, get,
    http::{header::ContentType, StatusCode},
    App, HttpResponse, HttpServer,
};
use derive_more::{Display, Error};

#[derive(Debug, Display, Error)]
enum UserError {
    #[display(fmt = "An internal error occurred. Please try again later.")]
    InternalError,
}

impl error::ResponseError for UserError {
    fn error_response(&self) -> HttpResponse {
        HttpResponse::build(self.status_code())
            .insert_header(ContentType::html())
            .body(self.to_string())
    }

    fn status_code(&self) -> StatusCode {
        match *self {
            UserError::InternalError => StatusCode::INTERNAL_SERVER_ERROR,
        }
    }
}

#[get("/")]
async fn index() -> Result<&'static str, UserError> {
    do_thing_that_fails().map_err(|_e| UserError::InternalError)?;
    Ok("success!")
}

1.1.6、日志记录

我们可以使用 middleware::Loagger 默认依赖env_logger 和 log ,来记录日志。

use actix_web::{error, get, middleware::Logger, App, HttpServer, Result};
use derive_more::{Display, Error};
use log::info;

#[derive(Debug, Display, Error)]
#[display(fmt = "my error: {}", name)]
pub struct MyError {
    name: &'static str,
}

// Use default implementation for `error_response()` method
impl error::ResponseError for MyError {}

#[get("/")]
async fn index() -> Result<&'static str, MyError> {
    let err = MyError { name: "test error" };
    info!("{}", err);
    Err(err)
}

#[rustfmt::skip]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "info");
    std::env::set_var("RUST_BACKTRACE", "1");
    env_logger::init();

    HttpServer::new(|| {
        let logger = Logger::default();

        App::new()
            .wrap(logger)
            .service(index)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

1.2、URL分发

1.2.1、URL分发介绍

URL分发提供了一个使用简单模式匹配语言将URL映射到处理程序代码的简单方法。如果其中一个个模式匹配与请求关联的路径信息,则调用特定的处理程序对象。

请求处理程序是一个函数,接收一个可以从 FromRequest 中提取零个或多个参数,并返回一个可以转换为 HttpResponse 的类型。

1.2.2、资源配置

资源配置像应用程序添加新资源的行为。资源有一个名称,他充当用于生成URL的标识符。该名称还允许开发人员向现有资源添加路由。资源也有一个模式,用来匹配URL的PATH部分。

App::route 方法提供了注册路由的简单方法,此方法面向应用程序路由表中添加单个路由。该方法接收一个路径模式、HTTP 方法和处理程序函数。route 方法可以为同一个路径调用多次,在这种情况下,多个路由注册相同的资源路径。

例子

use actix_web::{web, App, HttpResponse, HttpServer};

async fn index() -> HttpResponse {
    HttpResponse::Ok().body("Hello")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/", web::get().to(index))
            .route("/user", web::post().to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

虽然 App::route 提供了注册路由的简单方法,但要访问完整的资源配置,必须使用另一种方法。service() 方法将单个资源添加到应用程序路由表中。

如果资源没有包含任何路由或匹配路由,它将返回 NOT FOUND HTTP响应。

1.2.2.1、配置路由

资源包含一组路由,每个路由一次有一组守卫和一个处理程序。可以使用 Resource::route 方法创建新路由,该方法返回一个路由实例的引用。默认情况下,路由不包含任何守卫你,因此匹配所有请求,默认处理程序是
HttpNotFound。

应用程序根据在资源注册和路由注册期间定义的路由标准路由传入请求。 Resource 按照通过 Resource:: route 注册的路由顺序匹配所包含的所有的路由。

例子:一个路由可以包含任意数量的守卫,但是只能包含一个处理程序。

App::new().service(
    web::resource("/path").route(
        web::route()
            .guard(guard::Get())
            .guard(guard::Header("content-type", "text/plain"))
            .to(HttpResponse::Ok),
    ),
)

在上述例子中,如果请求包含 content-type 报头,且报头的值为 ext/plain ,并且 path 等于 /path,则返回HttpResponse::Ok

如果资源不能匹配任何路由,则返回 NOT FOUND 响应。 ResourceHandler::route 返回一个 route 对象。路由可以用类似于构建器的模式来配置。

  • Route::guard()注册一个新的保护。每条路线可以注册任意数量的警卫。
  • Route::method()注册一个方法保护。每条路线可以注册任意数量的警卫。
  • Route::to()为该路由注册了一个异步处理函数。只能注册一个处理程序。通常,处理程序注册是最后一个配置操作。

1.2.3、路由匹配

路由配置的主要目的是根据 URL 路径模式匹配请求的路径,Path 表示被请求URL的路径部分。Actix Web要做到这一点的方法非常简单,当请求进入系统时候,对于系统中存在的每个资源配置声明,根据声明的模式检查请求的路径。这种检查按照通过 App::service 方法声明路由的顺序进行的,如果找不到资源,则使用默认资源作为匹配的资源。

当路由配置声明时,可能包含路由守卫参数,所有路由声明相关联的路由保护必须为true,以便在检查期间将路由配置用于给定的请求,如果提供给路由配置的路由保护参数集合中的任何保护在检查期间返回false,则跳过该路由并通过有序的路由集继续进行路由匹配。

如果有任何路由匹配,路由匹配过程停止,并调用与该路由关联的处理程序,如果在用尽所有路由模式后,没有路由匹配,则返回NOT FOUND 响应。

1.2.4、资源匹配语法

Actix Web在模式参数重使用的模式匹配语言的语法很简单。路由配置中使用的模式可以以斜杆字符开头,如果模式不以斜杆字符开水,则匹配时,将在其前面加上隐式斜杆。

{foo}/bar/baz
/{foo}/bar/baz

上述两种写法是等价的。

变量部分以标识符的形式制定,这意味着接受除下一个斜杆字符以外的任何字符,并且将其作用 HttpRequest.match_info 对象中的名称。

模式中的替换标记匹配正则表达式 :[^{}/]+

match_info 是 Params 对象,表示基于路由模式从 URL中提取的动态部分,它可以通过 request.match_info 获取,例如下面模式定义了一个文字段和两个替换标志。

foo/{baz}/{bar}

上述模式匹配的URL将生成一下信息

foo/1/2        -> Params {'baz': '1', 'bar': '2'}
foo/abc/def    -> Params {'baz': 'abc', 'bar': 'def'}

下面URL将不匹配

foo/1/2/        -> No match (trailing slash)
bar/abc/def     -> First segment literal mismatch

段中的段替换标记的匹配将只在模式中的段中的第一个非字母、数字、字符之前完成。因此,例如,如果使用了一下路由模式

1.2.5、路由范围

路由范围可以帮助我们组织公共根路径的路由,可以在作用域内嵌套作用域。例如下面的Path

/usrs
/users/show
/users/show/{id}

我们可以使用下面的写法来组织路由

#[get("/show")]
async fn show_users() -> HttpResponse {
    HttpResponse::Ok().body("Show users")
}

#[get("/show/{id}")]
async fn user_detail(path: web::Path<(u32,)>) -> HttpResponse {
    HttpResponse::Ok().body(format!("User detail: {}", path.into_inner().0))
}

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

scope的路径可以包含可变路径段作为你资源与未使用作用域路径一致。可以从 HttpRequest:: match_info 中获取可变路径。路径提取器也能够提取范围级变量段。

1.2.6、匹配信息

所有表示匹配路径段段值都可以在 HttpRequest::match_info 中找到。可以使用 Path::get 检索特定的值。

use actix_web::{get, App, HttpRequest, HttpServer, Result};

#[get("/a/{v1}/{v2}/")]
async fn index(req: HttpRequest) -> Result<String> {
    let v1: u8 = req.match_info().get("v1").unwrap().parse().unwrap();
    let v2: u8 = req.match_info().query("v2").parse().unwrap();
    let (v3, v4): (u8, u8) = req.match_info().load().unwrap();
    Ok(format!("Values {} {} {} {}", v1, v2, v3, v4))
}

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

上述例子中 路径/a/1/2 将会解析为v1=1和v2=2;

1.2.6.1、路径信息提取器

Actix Web 提供了类型安全的路径信息提取器的功能,路径信息提取,目的类型可以用不同的形式定义,最简单的方法就是使用元组类型,元组中的每个元素必须对应路径模式中的一个元素,你可以匹配路径模式 /{id}/{username} 与 Path<(u32, String)> 类型,但是如果匹配Path<(String, String)> 则会失败。

例子:使用元组类型来进行路径信息提取

use actix_web::{get, web, App, HttpServer, Result};

#[get("/{username}/{id}/index.html")] // <- define path parameters
async fn index(info: web::Path<(String, u32)>) -> Result<String> {
    let info = info.into_inner();
    Ok(format!("Welcome {}! id: {}", info.0, info.1))
}

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

我们也可以抽取路径模式信息到结构体,结构体必需实现 Deserialize trait。

use actix_web::{get, web, App, HttpServer, Result};
use serde::Deserialize;

#[derive(Deserialize)]
struct Info {
    username: String,
}

// extract path info using serde
#[get("/{username}/index.html")] // <- define path parameters
async fn index(info: web::Path<Info>) -> Result<String> {
    Ok(format!("Welcome {}!", info.username))
}

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

Query 也同样提供了类似的功能。

1.2.7、生成资源URL

使用 HttpRequest.url_for 方法根据资源模式生成URL,HttpRequest.url_for 只能被命名资源调用,否则将会返回错误。

use actix_web::{get, guard, http::header, HttpRequest, HttpResponse, Result};

#[get("/test/")]
async fn index(req: HttpRequest) -> Result<HttpResponse> {
    let url = req.url_for("foo", ["1", "2", "3"])?; // <- generate url for "foo" resource

    Ok(HttpResponse::Found()
        .insert_header((header::LOCATION, url.as_str()))
        .finish())
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{web, App, HttpServer};

    HttpServer::new(|| {
        App::new()
            .service(
                web::resource("/test/{a}/{b}/{c}")
                    .name("foo") // <- set resource name, then it could be used in `url_for`
                    .guard(guard::Get())
                    .to(HttpResponse::Ok),
            )
            .service(index)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

上述代码,我们将会将 http://localhost:8080/test/ 地址重定向http://localhost:8080/test/1/2/3地址。

1.2.8、外部资源

有效的URL资源可以注册为外部资源,它们仅对URL生成有用,在请求时从不考虑进行匹配。

use actix_web::{get, App, HttpRequest, HttpServer, Responder};

#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
    let url = req.url_for("baidu", ["test"]).unwrap();

    url.to_string()
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(index)
            .external_resource("baidu", "https://www.baidu.com/s?wd={wd}")
    })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

1.2.9、路径规范化和重定向到斜杠附加到路由

路径规范化意味着

  • 1、在路径中添加尾斜杆
  • 2、用一个斜杆代替多个斜杆

处理程序一旦找到正确解析的路径就返沪i,如果所有条件都启用,则规范化条件的顺序是

  1. 合并
  2. 合并和追加
  3. 追加
    如果路径至少满足其中一个条件,它将会重定向到新路径。
use actix_web::{middleware, HttpResponse};

async fn index() -> HttpResponse {
    HttpResponse::Ok().body("Hello")
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{web, App, HttpServer};

    HttpServer::new(|| {
        App::new()
            .wrap(middleware::NormalizePath::default())
            .route("/resource/", web::to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

上述例子中,///resource/// 将会被重定向到 /resource/,本例子会为所有方法注册路径规范化纯利程序,但是您不应该依赖此机制来重定向 POST请求,重定向附加斜杆的Not Found 会把POST 请求转换为GET,丢失原始请求的POST数据。

use actix_web::{get, http::Method, middleware, web, App, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(middleware::NormalizePath::default())
            .service(index)
            .default_service(web::route().method(Method::GET))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
1.2.9.1、使用应用程序前缀组合应用程序

web::scope 方法允许设置一个特定的应用范围,这个作用域表示一个资源前缀,它将宝贝附加到资源配置添加到所有资源模式上,可以用来帮助在不同位置上挂载一组路由,而不是包含的可调用对象的作者想要的位置,同时仍然保持相同的资源名称。

#[get("/show")]
async fn show_users() -> HttpResponse {
    HttpResponse::Ok().body("Show users")
}

#[get("/show/{id}")]
async fn user_detail(path: web::Path<(u32,)>) -> HttpResponse {
    HttpResponse::Ok().body(format!("User detail: {}", path.into_inner().0))
}

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

在上面的例子中,show_users 路由将一个有效的路由模式 /users/show,而不是 show,因此应用程序的作用域将被添加到模式中,只有当URL路径为 /users/show 时,路由才会匹配,当 HttpRequest.url_for 以路由名称为 show_users 调用时,它将生成一个具有相同路径的URL。

1.2.10、自定义路由守卫

路由守卫可以看作是一个简单的函数,它接受请求对象引用并返回true或false,形式上,guard 是任何实现 guard 特性的对象。

use actix_web::{
    guard::{Guard, GuardContext},
    http, HttpResponse,
};

struct ContentTypeHeader;

impl Guard for ContentTypeHeader {
    fn check(&self, req: &GuardContext) -> bool {
        req.head()
            .headers()
            .contains_key(http::header::CONTENT_TYPE)
    }
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{web, App, HttpServer};

    HttpServer::new(|| {
        App::new().route(
            "/",
            web::route().guard(ContentTypeHeader).to(HttpResponse::Ok),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

在上述例子,只有当请求包含 CONTENT-TYPE 标头时才会调用索引处理程序。

use actix_web::{guard, web, App, HttpResponse, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().route(
            "/",
            web::route()
                .guard(guard::Not(guard::Get()))
                .to(HttpResponse::MethodNotAllowed),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}
1.2.10.1、修改守卫的值

你可以通过将任何动词值包装在Not中反转含义,例如你想要除了 GET 方法之外的所有方法都返回 METHOD NOT ALLOWED。

例子:

use actix_web::{guard, web, App, HttpResponse, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().route(
            "/",
            web::route()
                .guard(guard::Not(guard::Get()))
                .to(HttpResponse::MethodNotAllowed),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

我们可以也使用 Any 守卫接受一个列表来匹配任何允许的守卫,也可以使用 ALL 守卫接受任何匹配的值。

1.2.11、修改默认 NOT Found 响应

如果在路由表中找不到的路径模式,或者某个资源找不到匹配的路由,则使用默认资源,默认响应是 NOT FOUND,可以使用 App::default_service() 覆盖NOT FOUND响应,此方法接受一个与 App::service 相同的方法。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(web::resource("/").route(web::get().to(index)))
            .default_service(
                web::route()
                    .guard(guard::Not(guard::Get()))
                    .to(HttpResponse::MethodNotAllowed),
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

1.3、请求

1.3.1、JSON 请求

JSON 请求有几种实现方法

1.3.1.1、JSON 提取器

我们可以定义一个接受 JSON 作为参数的处理程序函数,然后注册这个处理程序,也可以通过使用 serde_json::Value 作为类型 T 来接受任意有效的 json 对象。

[dependencies]
actix-web = {version = "4.4.0"}
futures = "0.3.29"
serde = {version = "1.0.190", features = ["derive"]}
serde_json = "1.0.108"

例子:如果想添加默认值 可以参考文档:https://serde.rs/attr-default.html

use actix_web::{web, App, HttpServer, Result};
use serde::Deserialize;

#[derive(Deserialize)]
struct Info {
    username: String,
}

/// extract `Info` using serde
async fn index(info: web::Json<Info>) -> Result<String> {
    Ok(format!("Welcome {}!", info.username))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| App::new().route("/", web::post().to(index)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}
1.3.1.2、Payload 提取

您也手动将 payload 加载到内存中,然后反序列化

use actix_web::{error, post, web, App, Error, HttpResponse};
use futures::StreamExt;
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
struct MyObj {
    name: String,
    number: i32,
}

const MAX_SIZE: usize = 262_144; // max payload size is 256k

#[post("/")]
async fn index_manual(mut payload: web::Payload) -> Result<HttpResponse, Error> {
    // payload is a stream of Bytes objects
    let mut body = web::BytesMut::new();
    while let Some(chunk) = payload.next().await {
        let chunk = chunk?;
        // limit max size of in-memory payload
        if (body.len() + chunk.len()) > MAX_SIZE {
            return Err(error::ErrorBadRequest("overflow"));
        }
        body.extend_from_slice(&chunk);
    }

    // body is loaded, now we can deserialize serde-json
    let obj = serde_json::from_slice::<MyObj>(&body)?;
    Ok(HttpResponse::Ok().json(obj)) // <- send response
}

1.3.2、Content 编码

Actix Web 支持下面几种 payload 编解码。

  • Brotli:开源的一种新型的压缩算法,比Gzip压缩性能更好,支持的类型有text/xml、text/plain、text/css、application/javascript、application/x-javascript、application/rss+xml、text/javascript、image/tiff、image/svg+xml、application/json、application/xml
  • Gzip:
  • Deflate:
  • Zstd:

如果你的请求头包含 Content-Encoding 报头信息,则根据报头对请求负载进行解压缩。不支持多个值。

1.3.3、分块传输编码(Chunked transfer encoding)

Actix 会自动解码分块编码,web::Payloa 提取器已经包含了解码的字节流。如果请求负载使用了支持的压缩编解码器之一进行压缩,则字节流将被解压缩。

1.3.4、文件上传

Actix Web 支持 multipart 流,我们需要引入 actix-multipart crate。

1、添加依赖

 cargo add actix-multipart 

2、添加代码

use actix_multipart::{
    form::{
        tempfile::{TempFile, TempFileConfig},
        MultipartForm,
    },
};
use actix_web::{ web, App, Error, HttpResponse, HttpServer, Responder};
#[derive(Debug, MultipartForm)]
struct UploadForm {
    #[multipart(rename = "file")]
    files: Vec<TempFile>,
}

async fn save_files(MultipartForm(form): MultipartForm<UploadForm>) -> Result<impl Responder, Error> {
    for f in form.files {
        let path = format!("./tmp/{}", f.file_name.unwrap());
        f.file.persist(path).unwrap();
    }

    Ok(HttpResponse::Ok())
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::fs::create_dir_all("./tmp")?;
    println!("starting HTTP server at http://localhost:8080");

    HttpServer::new(|| {
        App::new()
            .app_data(TempFileConfig::default().directory("./tmp"))
            .service(web::resource("/")
                         .route(web::post()
                             .to(save_files)),
            )
    })
        .bind(("127.0.0.1", 8080))?
        .workers(2)
        .run()
        .await
}

1.3.5、URL编码的内容

Actix Web 提供了支持 application/x-www-form-urlencoded 编码的请求体,可以使用 Web::Form 提取器来接受反序列化实例。

下面几种情况可能会导致 UrlEncoded 特性出错

  • context-type 不是 application/x-www-form-urlencoded
  • transfer 编码是 chunked
  • content-length 大于 256k
  • payload 终止错误
use actix_web::{post, web, HttpResponse};
use serde::Deserialize;

#[derive(Deserialize)]
struct FormData {
    username: String,
}

#[post("/")]
async fn index(form: web::Form<FormData>) -> HttpResponse {
    HttpResponse::Ok().body(format!("username: {}", form.username))
}

1.3.6、流式请求

HttpRequest 是一个 Bytes 流,所以他可以使用 Payload来读取。

use actix_web::{get, web, Error, HttpResponse};
use futures::StreamExt;

#[get("/")]
async fn index(mut body: web::Payload) -> Result<HttpResponse, Error> {
    let mut bytes = web::BytesMut::new();
    while let Some(item) = body.next().await {
        let item = item?;
        println!("Chunk: {:?}", &item);
        bytes.extend_from_slice(&item);
    }

    Ok(HttpResponse::Ok().finish())
}

1.4、响应

一个类似建造器的模式用于构造 HttpResponse 实例HttpResponse 提供了几个返回 HttpResponseBuilder 实例的方法,该实例实现了用于构建响应的各种方便方法,

use actix_web::{http::header::ContentType, HttpResponse};

async fn index() -> HttpResponse {
    HttpResponse::Ok()
        .content_type(ContentType::plaintext())
        .insert_header(("X-Hdr", "sample"))
        .body("data")
}

1.4.1、JSON 响应

JSON 类型允许使用格式良好的 JSON 数据进行响应,只需要返回 JSON 的值。其中T就是序列为JSON 的结构的类型,类型T必须实习 serde 的 Serialize 特征。

1、添加依赖

serde = {version = "1.0.190", features = ["derive"]}

2、编写代码

use actix_web::{get, web, Responder, Result};
use serde::Serialize;

#[derive(Serialize)]
struct MyObj {
    name: String,
}

#[get("/a/{name}")]
async fn index(name: web::Path<String>) -> Result<impl Responder> {
    let obj = MyObj {
        name: name.to_string(),
    };
    Ok(web::Json(obj))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{App, HttpServer};

    HttpServer::new(|| App::new().service(index))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

1.4.2、Content 编码

Actix web 支持自动压缩,我们可以使用 压缩中间件 middleware::Compress。支持下列编码

  • Brotli
  • Gzip
  • Deflate
  • Identity
use actix_web::{get, middleware, App, HttpResponse, HttpServer};

#[get("/")]
async fn index() -> HttpResponse {
    HttpResponse::Ok().body("data")
}

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

响应头默认是:ContentEncoding::Auto,它是基于请求的Accept-Encoding头执行自动内容压缩协商的。可以通过将 Content-Encoding 设置为 Identity 值。

use actix_web::{
    get, http::header::ContentEncoding, middleware, App, HttpResponse, HttpServer,
};

#[get("/")]
async fn index() -> HttpResponse {
    HttpResponse::Ok()
        // v- disable compression
        .insert_header(ContentEncoding::Identity)
        .body("data")
}

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

我们也可以手动设置响应头,来绕过中间件

use actix_web::{
    get, http::header::ContentEncoding, middleware, App, HttpResponse, HttpServer,
};

static HELLO_WORLD: &[u8] = &[
    0x1f, 0x8b, 0x08, 0x00, 0xa2, 0x30, 0x10, 0x5c, 0x00, 0x03, 0xcb, 0x48, 0xcd, 0xc9, 0xc9,
    0x57, 0x28, 0xcf, 0x2f, 0xca, 0x49, 0xe1, 0x02, 0x00, 0x2d, 0x3b, 0x08, 0xaf, 0x0c, 0x00,
    0x00, 0x00,
];

#[get("/")]
async fn index() -> HttpResponse {
    HttpResponse::Ok()
        .insert_header(ContentEncoding::Gzip)
        .body(HELLO_WORLD)
}

1.5、测试

每个应用程序都应该经过良好的测试,Actix Web 提供了针对应用程序执行集成测试的工具,以及针对自定义提取器和中间件的担忧测试工具。

Actix Web 提供了请求构建器类型,TestRequest 实现了类型构建器的模式。您可以使用 to_http_request 生成一个 HttpRequest 实例,并用它调用处理程序或提取器。

1.5.1、集成测试

有几种方法可以测试应用测试,Actix Web 可用于在真实的HTTP服务器中运行带有特定处理程序的应用程序。

TestRequest::get 、TestRequest::post 和其他方法向测试服务器发送请求,要创建一个用于测试的服务,请使用 test::init_service 方法,该方法接受一个常规的应用构建器。

#[cfg(test)]
mod tests {
    use actix_web::{http::header::ContentType, test, App};

    use super::*;

    #[actix_web::test]
    async fn test_index_get() {
        let app = test::init_service(App::new().service(index)).await;
        let req = test::TestRequest::default()
            .insert_header(ContentType::plaintext())
            .to_request();
        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());
    }

    #[actix_web::test]
    async fn test_index_post() {
        let app = test::init_service(App::new().service(index)).await;
        let req = test::TestRequest::post().uri("/").to_request();
        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_client_error());
    }
}

如果你需要更富啊咋的应用程序配置,测试应该与创建普通应用程序非常相似。例如您可能需要初始化应用程序状态。创建一个带有 data 方法和附加状态的应用程序,就像你从一个普通应用程序中所做的那样。

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{test, web, App};

    #[actix_web::test]
    async fn test_index_get() {
        let app = test::init_service(
            App::new()
                .app_data(web::Data::new(AppState { count: 4 }))
                .service(index),
        )
        .await;
        let req = test::TestRequest::get().uri("/").to_request();
        let resp: AppState = test::call_and_read_body_json(&app, req).await;

        assert_eq!(resp.count, 4);
    }
}

1.5.2、流响应测试

如果你需要测试流生成,我们可以调用 into_parts 并将结果体转换为 future 并执行它就足够了,例如当我们测试SSE。

use std::task::Poll;

use actix_web::{
    http::{self, header::ContentEncoding, StatusCode},
    web, App, Error, HttpRequest, HttpResponse,
};
use futures::stream;

async fn sse(_req: HttpRequest) -> HttpResponse {
    let mut counter: usize = 5;

    // yields `data: N` where N in [5; 1]
    let server_events =
        stream::poll_fn(move |_cx| -> Poll<Option<Result<web::Bytes, Error>>> {
            if counter == 0 {
                return Poll::Ready(None);
            }
            let payload = format!("data: {}\n\n", counter);
            counter -= 1;
            Poll::Ready(Some(Ok(web::Bytes::from(payload))))
        });

    HttpResponse::build(StatusCode::OK)
        .insert_header((http::header::CONTENT_TYPE, "text/event-stream"))
        .insert_header(ContentEncoding::Identity)
        .streaming(server_events)
}

pub fn main() {
    App::new().route("/", web::get().to(sse));
}

#[cfg(test)]
mod tests {
    use super::*;

    use actix_web::{body, body::MessageBody as _, rt::pin, test, web, App};
    use futures::future;

    #[actix_web::test]
    async fn test_stream_chunk() {
        let app = test::init_service(App::new().route("/", web::get().to(sse))).await;
        let req = test::TestRequest::get().to_request();

        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());

        let body = resp.into_body();
        pin!(body);

        // first chunk
        let bytes = future::poll_fn(|cx| body.as_mut().poll_next(cx)).await;
        assert_eq!(
            bytes.unwrap().unwrap(),
            web::Bytes::from_static(b"data: 5\n\n")
        );

        // second chunk
        let bytes = future::poll_fn(|cx| body.as_mut().poll_next(cx)).await;
        assert_eq!(
            bytes.unwrap().unwrap(),
            web::Bytes::from_static(b"data: 4\n\n")
        );

        // remaining part
        for i in 0..3 {
            let expected_data = format!("data: {}\n\n", 3 - i);
            let bytes = future::poll_fn(|cx| body.as_mut().poll_next(cx)).await;
            assert_eq!(bytes.unwrap().unwrap(), web::Bytes::from(expected_data));
        }
    }

    #[actix_web::test]
    async fn test_stream_full_payload() {
        let app = test::init_service(App::new().route("/", web::get().to(sse))).await;
        let req = test::TestRequest::get().to_request();

        let resp = test::call_service(&app, req).await;
        assert!(resp.status().is_success());

        let body = resp.into_body();
        let bytes = body::to_bytes(body).await;
        assert_eq!(
            bytes.unwrap(),
            web::Bytes::from_static(b"data: 5\n\ndata: 4\n\ndata: 3\n\ndata: 2\n\ndata: 1\n\n")
        );
    }
}

1.5.3、单元测试

单元测试对应用程序的价值相当有限,但在开发提取器、中间件和响应器时非常有用。考虑到这一点,如果您想在自定义响应上进行断言,可以追杰调用独立定义的处理程序函数,而不使用路由宏。

#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::{
        http::{self, header::ContentType},
        test,
    };

    #[actix_web::test]
    async fn test_index_ok() {
        let req = test::TestRequest::default()
            .insert_header(ContentType::plaintext())
            .to_http_request();
        let resp = index(req).await;
        assert_eq!(resp.status(), http::StatusCode::OK);
    }

    #[actix_web::test]
    async fn test_index_not_ok() {
        let req = test::TestRequest::default().to_http_request();
        let resp = index(req).await;
        assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
    }
}

1.6、中间件(Middleware)

1.6.1、中间件介绍

Actix Web 的中间件系统允许我们在请求/响应处理中添加额外的行为。中间件可以与传入的请求进程挂钩,使我们能够秀姑请求以及暂停请求处理以提前返回响应。

中间件可以hook到响应处理,通常中间件涉及一下操作

  • 预处理请求
  • 后处理请求
  • 修改应用状态
  • 访问外部服务(redis、日志、会话)

中间件为每个应用程序、范围或资源注册,并以与注册相反的顺序执行。一般来说,中间件是一种实现 Service 特征和 Transform 特征的类型。每个方法都有一个默认实现,每个方法都可以立即返回结果或返回未来的对象。

use std::future::{ready, Ready};

use actix_web::{
    dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
    Error,
};
use futures_util::future::LocalBoxFuture;

// There are two steps in middleware processing.
// 1. Middleware initialization, middleware factory gets called with
//    next service in chain as parameter.
// 2. Middleware's call method gets called with normal request.
pub struct SayHi;

// Middleware factory is `Transform` trait
// `S` - type of the next service
// `B` - type of response's body
impl<S, B> Transform<S, ServiceRequest> for SayHi
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type InitError = ();
    type Transform = SayHiMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(SayHiMiddleware { service }))
    }
}

pub struct SayHiMiddleware<S> {
    service: S,
}

impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        println!("Hi from start. You requested: {}", req.path());

        let fut = self.service.call(req);

        Box::pin(async move {
            let res = fut.await?;

            println!("Hi from response");
            Ok(res)
        })
    }
}

对于简单的用例,可以使用 wrap_fn 来创建笑的,特别的中间件。

use actix_web::{dev::Service as _, web, App};
use futures_util::future::FutureExt;

#[actix_web::main]
async fn main() {
    let app = App::new()
        .wrap_fn(|req, srv| {
            println!("Hi from start. You requested: {}", req.path());
            srv.call(req).map(|res| {
                println!("Hi from response");
                res
            })
        })
        .route(
            "/index.html",
            web::get().to(|| async { "Hello, middleware!" }),
        );
}

1.6.2、日志中间件

日志是作为中间件实现的,通常将日志中间件注册为应用程序的第一个中间件,必须为每个应用程序注册日志中间件。

Logger 中间件使用标准日志箱记录信息,您应该为actix web 包启用记录器以表查看访问日志(env_logger或类似)。

1.6.2.1、使用

使用指定的格式创建 Logger 中间件。默认记录器可以用默认方法创建,使用默认的格式。

%a %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %T

例子:

use actix_web::middleware::Logger;
use env_logger::Env;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{App, HttpServer};

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

    HttpServer::new(|| {
        App::new()
            .wrap(Logger::default())
            .wrap(Logger::new("%a %{User-Agent}i"))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

日志格式

INFO:actix_web::middleware::logger: 127.0.0.1:59934 [02/Dec/2017:00:21:43 -0800] "GET / HTTP/1.1" 302 0 "-" "curl/7.54.0" 0.000397
INFO:actix_web::middleware::logger: 127.0.0.1:59947 [02/Dec/2017:00:22:40 -0800] "GET /index.html HTTP/1.1" 200 0 "-" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:57.0) Gecko/20100101 Firefox/57.0" 0.000646
1.6.2.2、格式
  • %% 百分号;
  • %a 远程IP地址(使用反向代理时为代理IP地址)
  • %t 开始处理请求的时间
  • %P 服务请求的子进程的进程ID
  • %r 请求的第一行
  • %s 响应状态码
  • %b 响应大小(以字节为单位),包括 HTTP 头
  • %T 处理请求所需要的时间,以秒为单位,带有 0.06f 格式的浮动分数
  • %D 服务请求所需的时间,以毫米为单位
  • %{FOO}i request.headers[‘FOO’]
  • %{FOO}o response.headers[‘FOO’]
  • %{FOO}e os.environ[‘FOO’]

1.6.3、Default headers

要设置默认的响应头,我们可以使用 DefaultHeaders 中间件。如果响应头已经包含指定的头,DefaultHeaders 中间件不会设置头。

use actix_web::{http::Method, middleware, web, App, HttpResponse, HttpServer};

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2")))
            .service(
                web::resource("/test")
                    .route(web::get().to(HttpResponse::Ok))
                    .route(web::method(Method::HEAD).to(HttpResponse::MethodNotAllowed)),
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

1.6.4、用户会话

Actix Web 提供了会话管理的通用解决方案,使用 actix-session 中间件可以使用多种后段类型来存储会话数据。默认实现cookie会话后段,也可以添加其他后段实现。

CookieSession 使用 cookie 作为会话存储,CookieSessionBackend 创建的会话呗限制为存储少于4000字节的数据,因为 payload 必须适合单个 cookie,如果会话长度超过 4000 字节,会产生服务器内部错误。

可能具有已签名或私有的安全策略,每个都有一个各自的 CookieSession 构造器。

签名的 cookie 可以被查看,但不能被客户端修改,客户端既不能查看也不能修改私有 cookie。构造函数接受一个键作为参数,这是 cookie 会话的私钥——当这个值被改变,所有会话数据都会消失。

通常,您可以创建 SessionStorage 中间件并使用特定的后段实现(如 cookiesession)对其进行初始化,要访问会话数据,必须使用会话提取器,这个方法返回一个Session 对象,它允许我们获取或设置会话数据。

1、添加依赖

cargo add actix-session

2、例子代码

use actix_session::{Session, SessionMiddleware, storage::CookieSessionStore};
use actix_web::{web, App, Error, HttpResponse, HttpServer, cookie::Key};

async fn index(session: Session) -> Result<HttpResponse, Error> {
    // access session data
    if let Some(count) = session.get::<i32>("counter")? {
        session.insert("counter", count + 1)?;
    } else {
        session.insert("counter", 1)?;
    }

    Ok(HttpResponse::Ok().body(format!(
        "Count is {:?}!",
        session.get::<i32>("counter")?.unwrap()
    )))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(
                // create cookie based session middleware
                SessionMiddleware::builder(CookieSessionStore::default(), Key::from(&[0; 64]))
                    .cookie_secure(false)
                    .build()
            )
            .service(web::resource("/").to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

1.6.5、错误处理

ErrorHandlers 中间件允许我们为响应提供自定义处理程序,您可以使用 ErrorHandlers::handler 方法为特定状态码注册自定义错误处理程序。您可以修改现有的响应或创建一个全新的响应。错误处理程序可以立即返回响应,也可以返回解析为响应的future。

use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers};
use actix_web::{
    dev,
    http::{header, StatusCode},
    web, App, HttpResponse, HttpServer, Result,
};

fn add_error_header<B>(mut res: dev::ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
    res.response_mut().headers_mut().insert(
        header::CONTENT_TYPE,
        header::HeaderValue::from_static("Error"),
    );

    Ok(ErrorHandlerResponse::Response(res.map_into_left_body()))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .wrap(
                ErrorHandlers::new()
                    .handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header),
            )
            .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError)))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

1.7、静态文件

1.7.1、单个文件

可以使用自定义路径模式和 NamedFile 来提供静态文件。为了匹配路径尾部,我们可以使用 [.*] 正则表达式。

use actix_files::NamedFile;
use actix_web::{HttpRequest, Result};
use std::path::PathBuf;

async fn index(req: HttpRequest) -> Result<NamedFile> {
    let path: PathBuf = req.match_info().query("filename").parse().unwrap();
    Ok(NamedFile::open(path)?)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    use actix_web::{web, App, HttpServer};

    HttpServer::new(|| App::new().route("/{filename:.*}", web::get().to(index)))
        .bind(("127.0.0.1", 8080))?
        .run()
        .await
}

注意 将路径尾部与 [.*] 正则使用返回一个 NamedFile 有安全隐患,它为攻击者提供者插入并访问主机上运行服务器的用户有权访问的每个文件。

1.7.2、目录

为了从特定目录和子目录中提供文件,可以使用文件。文件时必须用 App::service 访问注册,否则它将无法为 自路径提供服务。

use actix_files as fs;
use actix_web::{App, HttpServer};

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

默认情况下,禁用子目录的文件列表,尝试加载目录列表将返回 404 Not Found 响应。要启用文件列表,使用 fles::show_files_liisting() 方法。

可以重定向到特定的索引文件,而不是显示目录的文件列表。使用 Files::index_file 方法配置此重定向。

1.7.3、配置

NamedFiles 可以为服务文件指定各种选项

  • set_content_disposition 函数用于将文件的 MIME 映射到相应的内容处理类型
  • use_etag 指定是否计算 ETag 并将其包含在 header 中;
  • use_last_modified 指定是否使用文件修改后的时间戳,并将其添加到 Last-Modified 头中
use actix_files as fs;
use actix_web::http::header::{ContentDisposition, DispositionType};
use actix_web::{get, App, Error, HttpRequest, HttpServer};

#[get("/{filename:.*}")]
async fn index(req: HttpRequest) -> Result<fs::NamedFile, Error> {
    let path: std::path::PathBuf = req.match_info().query("filename").parse().unwrap();
    let file = fs::NamedFile::open(path)?;
    Ok(file
        .use_last_modified(true)
        .set_content_disposition(ContentDisposition {
            disposition: DispositionType::Attachment,
            parameters: vec![],
        }))
}

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

配置也可以引用到目录

use actix_files as fs;
use actix_web::{App, HttpServer};

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

二、WebSocket

Actix-Web 同样也支持 WebSocket,使用 actix-web-actors 库即可。它可以将 Payload 转换为带有 web::Payload 到 ws::Message 流,然后使用流组合子来处理实际的消息,但是使用 http actor 处理 websocket 通信更简单。

1、查看依赖

cargo add actix-web-actors

2、代码

use actix::{Actor, StreamHandler};
use actix_web::{web, App, Error, HttpRequest, HttpResponse, HttpServer};
use actix_web_actors::ws;

/// Define HTTP actor
struct MyWs;

impl Actor for MyWs {
    type Context = ws::WebsocketContext<Self>;
}

/// Handler for ws::Message message
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
    fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
        match msg {
            Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
            Ok(ws::Message::Text(text)) => ctx.text(text),
            Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
            _ => (),
        }
    }
}

async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
    let resp = ws::start(MyWs {}, &req, stream);
    println!("{:?}", resp);
    resp
}

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

三、热加载(Auto-Reloading)

在开发过程中,让 cargo 在发生更改时自动重新编译代码是非常方便的。可以使用 cargo-watch 完成。

 cargo watch -x run

四、Shuttle

Shuttle 是一个 Rust 云原生开发平台,可以让我们免费部署 Rust 应用程序。Shuttle 对 Actix Web 具有开箱即用的支持,按照一下步骤在 Shuttle 上托管 Web 服务。

1、添加依赖

cargo add shuttle-actix-web shuttle-runtime

2、修改 main 函数

use actix_web::{get, web::ServiceConfig};
use shuttle_actix_web::ShuttleActixWeb;

#[shuttle_runtime::main]
async fn main() -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> {
    let config = move |cfg: &mut ServiceConfig| {
        // set up your service here, e.g.:
        cfg.service(hello_world);
    };

    Ok(config.into())
}

3、安装 cargo-shuttle

cargo install cargo-shuttle

4、在 shuttle 平台上创建项目

cargo shuttle project start

5、部署

cargo shuttle deploy

总结

本章节讲解 Actix-Web 的进阶使用。

你可能感兴趣的:(跟小嘉学,Rust,编程,rust,前端,开发语言,设计模式,web,后端)