类型安全的信息提取

extractors.md
commit - 4d8d53cea59bca095ca5c02ef81f0b1791736855 - 2020.09.12

actix-web 提供了一个称之为提取器(extractor,impl FromRequest)的请求信息访问工具,它是类型安全的。默认情况下,actix-web 提供了多种提取器实现。

提取器可以作为 handler 函数的参数。actix-web 支持每个 handler 函数最多有 10 个提取器参数,参数位置无关紧要。

async fn index(path: web::Path<(String, String)>, json: web::Json<MyInfo>) -> impl Responder {
    let path = path.into_inner();
    format!("{} {} {} {}", path.0, path.1, json.id, json.username)
}

路径(Path)

路径(Path)结构体提供可从请求路径提取的信息,路径中的任何变量都可以反序列化。

举例来说,对于注册为 /users/{user_id}/{friend} 路径的资源,有两个变量可以被反序列化:user_idfriend。这些变量可以被提取到一个元组(tuple)中(如 Path<(u32, String)>),或者被提取到实现了 serde crate 中的 Deserialize trait 的任何结构中。

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

/// extract path info from "/users/{user_id}/{friend}" url
/// {user_id} - deserializes to a u32
/// {friend} - deserializes to a String
#[get("/users/{user_id}/{friend}")] // <- define path parameters
async fn index(web::Path((user_id, friend)): web::Path<(u32, String)>) -> Result<String> {
    Ok(format!("Welcome {}, user_id {}!", friend, user_id))
}

#[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
}

路径信息也可以提取到实现了 serde crate 中的 Deserialize trait 的特定类型中。下面是一个使用*结构体(struct)类型而非元组(tuple)类型的例子,结构体类型实现了 serde crate 中的 Deserialize trait,它和使用元组(tuple)*类型是等效的。

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

#[derive(Deserialize)]
struct Info {
    user_id: u32,
    friend: String,
}

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

#[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
}

还可以使用 get 方法或者 query 方法,根据参数名称提取请求中的路径参数:

#[get("/users/{userid}/{friend}")] // <- define path parameters
async fn index(req: HttpRequest) -> Result<String> {
    let name: String = req.match_info().get("friend").unwrap().parse().unwrap();
    let userid: i32 = req.match_info().query("userid").parse().unwrap();

    Ok(format!("Welcome {}, userid {}!", name, userid))
}

#[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
}

查询(Query)

查询(Query)结构体为请求中的查询参数提供提取功能。下文的例子使用了 serde_urlencoded crate:

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

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

// this handler get called only if the request's query contains `username` field
#[get("/")]
async fn index(info: web::Query<Info>) -> String {
    format!("Welcome {}!", info.username)
}

Json

Json 结构体允许将请求体反序列化为结构体。要从请求体中提取类型化的信息,则类型 T 必须实现 serde crate 中的 Deserialize trait。

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

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

/// deserialize `Info` from request's body
#[get("/")]
async fn index(info: web::Json<Info>) -> Result<String> {
    Ok(format!("Welcome {}!", info.username))
}

一些提取器提供了配置提取过程的方法,JsonConfig 结构体用于配置 Json 提取器。要配置提取器,请将其配置对象传递给 web::resource.app_data() 方法。配置后,Json 提取器将返回 JsonConfig 结构体。你也可以配置 json 有效负载的最大值,以及自定义错误处理函数。

下面的示例中,将有效负载的大小限制为 4kb,并使用自定义的错误 handler

use actix_web::{error, web, App, HttpResponse, HttpServer, Responder};
use serde::Deserialize;

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

/// deserialize `Info` from request's body, max payload size is 4kb
async fn index(info: web::Json<Info>) -> impl Responder {
    format!("Welcome {}!", info.username)
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        let json_config = web::JsonConfig::default()
            .limit(4096)
            .error_handler(|err, _req| {
                // create custom error response
                error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
            });

        App::new().service(
            web::resource("/")
                // change json extractor configuration
                .app_data(json_config)
                .route(web::post().to(index)),
        )
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

表单(Form)

目前,仅支持 url 编码的表单。url 编码的主体信息可以被提取为特定类型,此类型必须实现 serde crate 中的 Deserialize trait。

FormConfig 结构体允许配置提取过程。

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

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

/// extract form data using serde
/// this handler gets called only if the content type is *x-www-form-urlencoded*
/// and the content of the request could be deserialized to a `FormData` struct
#[post("/")]
async fn index(form: web::Form<FormData>) -> Result<String> {
    Ok(format!("Welcome {}!", form.username))
}

其它

actix-web 还提供了其它几种提取器:

  • Data - 如果需要访问应用程序状态。
  • HttpRequest - HttpRequest 自身既是提取器,它返回 self,以便于你访问请求。
  • String - 你可以转换请求的有效负载为 字符串(String) 类型。请参阅文档字符串实例
  • bytes::Bytes - 你可以转换请求的有效负载为 Bytes 类型。请参阅文档字符串实例
  • Payload - 你可以访问请求的有效负载。请参阅实例

应用状态提取器

可以使用 web::Data 提取器,从请求 handler 访问应用程序状态;但是,状态仅可以作为只读引用访问。如果你需要对状态的可变(mutable)访问,则状态必须被实现。

注意,actix 会创建应用程序状态和请求 handler 的多个副本,它为每个工作线程创建一个副本。

下面是一个请求 handler 的示例,用于存储已处理的请求数:

use actix_web::{web, Responder};
use std::cell::Cell;

#[derive(Clone)]
struct AppState {
    count: Cell<i32>,
}

async fn show_count(data: web::Data<AppState>) -> impl Responder {
    format!("count: {}", data.count.get())
}

async fn add_one(data: web::Data<AppState>) -> impl Responder {
    let count = data.count.get();
    data.count.set(count + 1);

    format!("count: {}", data.count.get())
}

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

    let data = AppState {
        count: Cell::new(0),
    };

    HttpServer::new(move || {
        App::new()
            .data(data.clone())
            .route("/", web::to(show_count))
            .route("/add", web::to(add_one))
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

尽管此 handler 可以运行,但依赖于线程数和每个线程处理的请求数因素,self.0 可能不正确。正确的实现应该使用 Arc(原子引用计数器)AtomicUsize

use actix_web::{get, web, App, HttpServer, Responder};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;

#[derive(Clone)]
struct AppState {
    count: Arc<AtomicUsize>,
}

#[get("/")]
async fn show_count(data: web::Data<AppState>) -> impl Responder {
    format!("count: {}", data.count.load(Ordering::Relaxed))
}

#[get("/add")]
async fn add_one(data: web::Data<AppState>) -> impl Responder {
    data.count.fetch_add(1, Ordering::Relaxed);

    format!("count: {}", data.count.load(Ordering::Relaxed))
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let data = AppState {
        count: Arc::new(AtomicUsize::new(0)),
    };

    HttpServer::new(move || {
        App::new()
            .data(data.clone())
            .service(show_count)
            .service(add_one)
    })
    .bind("127.0.0.1:8080")?
    .run()
    .await
}

actix-web 框架异步地处理请求,请小心使用诸如 Mutex 或者 RwLock 之类的同步原语。 如果阻止了线程执行,所有并发请求处理进程都将阻塞。 若你需要从多个线程共享或更新某些状态,请考虑使用 tokio crate 的同步原语。