actix_web官方文档翻译

基础

开始

安装Rust

如果你还没有rust,我们推荐你使用rustup管理rust安装。官网有

official rust guide安装指南。

Actix Web最低支持版本 1.54。运行rustup更新将确保您有最新和最好的Rust版本可用。因此,本指南假设您正在运行Rust 1.54或更高版本。

Hello, world!

创建一个Cargo project:

cargo new hello-world
cd hello-world

增加actix-web依赖在Cargo.toml文件。

[dependencies]
actix-web = "4"

请求处理程序使用接受零个或多个参数的异步函数。
可以从请求中提取这些参数(见FromRequest trait)并返回一个可以转换为HttpResponse的类型(参见Responder trait):

use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};

#[get("/")]
async fn hello() -> impl Responder {
    HttpResponse::Ok().body("Hello world!")
}

#[post("/echo")]
async fn echo(req_body: String) -> impl Responder {
    HttpResponse::Ok().body(req_body)
}

async fn manual_hello() -> impl Responder {
    HttpResponse::Ok().body("Hey there!")
}

注意,其中一些处理程序使用内置宏直接附加了路由信息。这允许您指定处理程序应该响应的方法和路径。您将在下面看到如何注册manual_hello(例如,没有使用路由宏的路由)。

接下来,创建一个App实例并注册请求处理程序。对于使用路由宏的处理程序使用App::service,对于手动路由的处理程序使用App::route,声明路径和方法。最后,应用程序在HttpServer中启动,HttpServer将使用你的应用程序作为“应用程序工厂“来服务传入的请求。

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

用 cargo run 就完成编译运行程序,#[actix_web::main]宏在actix运行时中执行async main函数。现在,您可以访问http://127.0.0.1:8080/或您定义的任何其他路由以查看结果。

应用

actix-web提供各种组件来使用Rust构建web服务器和应用程序。它提供路由、中间件、请求的预处理、响应的后处理等。

所有的actix-web服务器都是围绕App实例构建的。用于为资源和中间件注册路由。它还存储同一作用域内所有处理程序共享的应用程序状态。

一个应用程序的作用域充当所有路由的命名空间,也就是说,一个特定应用程序作用域的所有路由都具有相同的url路径前缀。应用程序前缀总是包含一个前导“/”斜杠。如果提供的前缀不包含前导斜杠,则会自动插入它。前缀应该由值路径段组成。

对于一个范围为/app的应用程序,任何路径为/app, /app/或/app/test的请求都将匹配;但是,路径/application程序不匹配。

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

async fn index() -> impl Responder {
    "Hello world!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(
            // prefixes all resources and routes attached to it...
            web::scope("/app")
                // ...so this handles requests for `GET /app/index.html`
                .route("/index.html", web::get().to(index)),
        )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

在本例中,创建了一个带有/app前缀和index.html资源的应用程序。这个资源可以通过/app/index.html url获得。

有关更多信息,请查看URL Dispatch 部分。

状态

应用程序状态与同一范围内的所有路由和资源共享。State可以通过web::Data提取器访问,其中T是状态的类型。中间件也可以访问State。

让我们编写一个简单的应用程序,并将应用程序名称存储在状态中:

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

// This struct represents state
struct AppState {
    app_name: String,
}

#[get("/")]
async fn index(data: web::Data<AppState>) -> String {
    let app_name = &data.app_name; // <- get app_name
    format!("Hello {app_name}!") // <- response with app_name
}

接下来,在初始化应用程序时传递这个状态,并启动应用程序:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .app_data(web::Data::new(AppState {
                app_name: String::from("Actix Web"),
            }))
            .service(index)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

可以在应用程序中注册任意数量的状态类型:

共享可变状态

HttpServer接受应用程序工厂而不是应用程序实例。一个HttpServer为每个线程构造一个应用程序实例。因此,必须多次构造应用程序数据。如果你想在不同的线程之间共享数据,应该使用一个可共享的对象。比如Send + Sync.

在内部,web::Data使用Arc所以为了避免创建两个arc,我们应该在注册使用之前创建我们的Data ,使用App::app_data().

在下面的例子中,我们将编写一个具有可变、共享状态的应用程序。首先,我们定义状态并创建处理程序:

use actix_web::{web, App, HttpServer};
use std::sync::Mutex;

struct AppStateWithCounter {
    counter: Mutex<i32>, // <- Mutex is necessary to mutate safely across threads
}

async fn index(data: web::Data<AppStateWithCounter>) -> String {
    let mut counter = data.counter.lock().unwrap(); // <- get counter's MutexGuard
    *counter += 1; // <- access counter inside MutexGuard

    format!("Request number: {counter}") // <- response with count
}

并将数据注册到App:

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Note: web::Data created _outside_ HttpServer::new closure
    let counter = web::Data::new(AppStateWithCounter {
        counter: Mutex::new(0),
    });

    HttpServer::new(move || {
        // move counter into the closure
        App::new()
            .app_data(counter.clone()) // <- register the created data
            .route("/", web::get().to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

关键点:

  • 在传递给HttpServer::new的闭包中初始化的状态对工作线程来说是本地的,如果修改可能会变成非同步的。
  • 为了实现全局共享状态,它必须在传递给HttpServer::new并移动/克隆进去的闭包之外创建。

使用应用程序范围组合应用程序

web::scope() 方法允许设置资源组前缀。他的范围表示一个资源前缀,它将作为资源配置添加的所有资源模式的前缀。这可以用来帮助将一组路由挂载到与原始作者意图不同的位置,同时仍然保持相同的资源名称。

举例:

#[actix_web::main]
async fn main() {
    let scope = web::scope("/users").service(show_users);
    App::new().service(scope);
}

在上面的例子中,show_users路由的有效路由模式是/users/show而不是/show,因为应用程序的scope参数会放在模式的前面。只有当URL路径为/users/show时,该路由才会匹配,当HttpRequest.url_for()函数以路由名show_users调用时,它将生成一个具有相同路径的URL。

应用程序保护和虚拟主机

您可以将守卫看作一个简单的函数,它接受请求对象引用并返回true或false。形式上,守卫是任何实现守卫特征的对象。
Actix Web提供了几种保护措施。你可以查看API文档的函数部分。

Header是提供的守卫之一。它可以用作基于请求头信息的过滤器。

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(
                web::scope("/")
                    .guard(guard::Header("Host", "www.rust-lang.org"))
                    .route("", web::to(|| async { HttpResponse::Ok().body("www") })),
            )
            .service(
                web::scope("/")
                    .guard(guard::Header("Host", "users.rust-lang.org"))
                    .route("", web::to(|| async { HttpResponse::Ok().body("user") })),
            )
            .route("/", web::to(HttpResponse::Ok))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

配置

了简单和可重用,App和web::Scope都提供了configure方法。这个函数对于将配置的一部分移动到不同的模块甚至库非常有用。例如,资源的一些配置可以移动到不同的模块。

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

// this function could be located in a different module
fn scoped_config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::resource("/test")
            .route(web::get().to(|| async { HttpResponse::Ok().body("test") }))
            .route(web::head().to(HttpResponse::MethodNotAllowed)),
    );
}

// this function could be located in a different module
fn config(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::resource("/app")
            .route(web::get().to(|| async { HttpResponse::Ok().body("app") }))
            .route(web::head().to(HttpResponse::MethodNotAllowed)),
    );
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .configure(config)
            .service(web::scope("/api").configure(scoped_config))
            .route(
                "/",
                web::get().to(|| async { HttpResponse::Ok().body("/") }),
            )
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

上面例子的结果是:

/         -> "/"
/app      -> "app"
/api/test -> "test"

每个ServiceConfig 都有它自己的data,routes和services。

服务器

HTTP服务器

HttpServer 类型负责服务HTTP请求。

HttpServer接受一个应用程序工厂作为参数,应用程序工厂必须有Send+Sync限定。

有关这方面的更多内容,请参阅多线程部分。

要启动web服务器,它必须首先绑定到一个网络套接字。使用HttpServer::bind()使用套接字地址元组或字符串("127.0.0.1", 8080)"0.0.0.0:8080". 如果套接字正在被另一个应用程序使用,则此操作将失败。

绑定成功后,使用HttpServer::run()返回一个Server实例。服务器必须await或spawn以开始处理请求,并将运行直到接收到关闭信号。(例如,默认情况下,ctrl-c read more here))

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

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

多线程

HttpServer自动启动一定数量的HTTP worker,默认情况下,这个数量等于系统中逻辑cpu的数量。这个数字可以用HttpServer::workers()方法重写。

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

#[actix_web::main]
async fn main() {
    HttpServer::new(|| App::new().route("/", web::get().to(HttpResponse::Ok))).workers(4);
    // <- Start 4 workers
}

一旦创建了工作者,它们每个都接收一个单独的应用程序实例来处理请求。
应用程序状态不能在线程之间共享,处理程序可以自由地操作状态的副本,而不涉及并发性问题。

应用程序状态不需要是Send或Sync,但应用程序工厂必须是Sync+Send。

为了在工作线程之间共享状态,使用Arc/Data。一旦引入共享和同步,就应该特别注意。在许多情况下,由于锁定共享状态以进行修改,会无意中引入性能成本。

在某些情况下,这些成本可以通过使用更有效的锁定策略来缓解,例如,使用读/写锁而不是互斥锁来实现非互斥锁,但性能最高的实现往往是根本不发生锁的实现。

由于每个工作线程顺序处理它的请求,阻塞当前线程的处理程序将导致当前工作线程停止处理新请求:

fn my_handler() -> impl Responder {
    std::thread::sleep(Duration::from_secs(5)); // <-- Bad practice! Will cause the current worker thread to hang!
    "response"
}

因此,任何长时间的、非cpu绑定的操作(例如I/O、数据库操作等)应该表示为futures或异步函数。Async处理程序由工作线程并发执行,因此不会阻塞执行:

async fn my_handler() -> impl Responder {
    tokio::time::delay_for(Duration::from_secs(5)).await; // <-- Ok. Worker thread will handle other requests here
    "response"
}

同样的限制也适用于提取器。当处理函数接收到实现FromRequest的参数时,这个实现阻塞了当前线程,工作线程在运行处理程序时会阻塞。出于这个原因,在实现提取器时必须特别注意,并且在需要的地方也应该异步实现提取器。

TLS / HTTPS

Actix Web支持两种开箱即用的TLS实现:rustls和openssl。

rustls crate 特性用于rustls整合 而 openssl用于openssl整合。

[dependencies]
actix-web = { version = "4", features = ["openssl"] }
openssl = { version = "0.10" }
use actix_web::{get, App, HttpRequest, HttpServer, Responder};
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};

#[get("/")]
async fn index(_req: HttpRequest) -> impl Responder {
    "Welcome!"
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // load TLS keys
    // to create a self-signed temporary cert for testing:
    // `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
    let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
    builder
        .set_private_key_file("key.pem", SslFiletype::PEM)
        .unwrap();
    builder.set_certificate_chain_file("cert.pem").unwrap();

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

创建密钥。cert.pem和cert.pem使用该命令。用它添加你的项目:

$ openssl req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem \
  -days 365 -sha256 -subj "/C=CN/ST=Fujian/L=Xiamen/O=TVlinux/OU=Org/CN=muro.lxd"

若要删除密码,请立即复制。pem, key.pem

$ openssl rsa -in key.pem -out nopass.pem

保持连接

Actix Web保持连接打开以等待后续请求。保持连接行为是由服务器设置定义的。

  • Duration::from_secs(75) or KeepAlive::Timeout(75):开启75秒的保持连接计时器。

  • KeepAlive::Os:使用 OS keep-alive。

  • None 或者 KeepAlive::Disabled: 关闭keep-alive.

use actix_web::{http::KeepAlive, HttpServer};
use std::time::Duration;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // Set keep-alive to 75 seconds
    let _one = HttpServer::new(app).keep_alive(Duration::from_secs(75));

    // Use OS's keep-alive (usually quite long)
    let _two = HttpServer::new(app).keep_alive(KeepAlive::Os);

    // Disable keep-alive
    let _three = HttpServer::new(app).keep_alive(None);

    Ok(())
}

如果选择了上面的第一个选项,那么如果响应没有显式地禁止它,则对HTTP/1.1请求启用keep-alive,举个例子,将connection type 设置为Close或Upgrade。强制关闭连接可以通过HttpResponseBuilder上的force_close()方法来完成。

Keep-alive对于HTTP/1.0是关闭的,对于HTTP/1.1和HTTP/2.0是打开的。

优雅的关机

HttpServer支持优雅的关机。在收到停止信号后,工作人员有一段特定的时间来完成服务请求。任何在超时后还活着的工人都将被强制删除。默认情况下,关机超时时间设置为30秒。
你可以通过HttpServer::shutdown_timeout()方法来改变这个参数。

HttpServer处理多个操作系统信号。CTRL-C 在所有操作系统上可用,其他信号在unix系统上可用。

SIGINT 强制关闭工作

SIGTERM 优雅的关闭工作

SIGQUIT 强制关闭工作

可以使用禁用信号处理HttpServer::disable_signals() 方法。

处理器

请求处理器

请求处理程序是一个异步函数,它接受零个或多个参数,这些参数可以从请求中提取(如 impl FromRequest)然后再返回一个可以转化为 HttpResponse 的类型(如 impl Responder)。

请求处理分为两个阶段。首先调用处理程序对象,返回实现Responder trait的接口,然后在返回的对象上调用Respond_to (),,将自身转换为HttpResponse或Error。

默认情况下,actix-web为一些标准类型提供了Responder实现,例如&'static str, String等。要获得实现的完整列表,请检查 Responder documentation.

有效的处理程序示例:

async fn index(_req: HttpRequest) -> &'static str {
    "Hello world!"
}
async fn index(_req: HttpRequest) -> String {
    "Hello world!".to_owned()
}

您还可以将签名更改为返回impl Responder,如果涉及到更复杂的类型,它会很好地工作。

async fn index(_req: HttpRequest) -> impl Responder {
    web::Bytes::from_static(b"Hello world!")
}
async fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
    ...
}

使用自定义类型的响应

要从处理程序函数直接返回自定义类型,该类型需要实现Responder trait。

让我们创建一个自定义类型的响应,序列化为一个application/json响应:

use actix_web::{
    body::BoxBody, http::header::ContentType, HttpRequest, HttpResponse, Responder,
};
use serde::Serialize;

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

// Responder
impl Responder for MyObj {
    type Body = BoxBody;

    fn respond_to(self, _req: &HttpRequest) -> HttpResponse<Self::Body> {
        let body = serde_json::to_string(&self).unwrap();

        // Create response and set content type
        HttpResponse::Ok()
            .content_type(ContentType::json())
            .body(body)
    }
}

async fn index() -> impl Responder {
    MyObj { name: "user" }
}

流响应主体

响应体可以异步生成。在这种情况下,body必须实现流trait stream <Item=Bytes, Error=Error>,即。

use actix_web::{get, web, App, Error, HttpResponse, HttpServer};
use futures::{future::ok, stream::once};

#[get("/stream")]
async fn stream() -> HttpResponse {
    let body = once(ok::<_, Error>(web::Bytes::from_static(b"test")));

    HttpResponse::Ok()
        .content_type("application/json")
        .streaming(body)
}

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

不同的返回类型(Either)

有时,您需要返回不同类型的响应。例如,您可以进行错误检查并返回错误、返回异步响应或任何需要两种不同类型的结果。

对于这种情况,可以使用Either类型。两者都允许将两种不同的响应器类型组合成一个类型。

use actix_web::{Either, Error, HttpResponse};

type RegisterResult = Either<HttpResponse, Result<&'static str, Error>>;

async fn index() -> RegisterResult {
    if is_a_variant() {
        // choose Left variant
        Either::Left(HttpResponse::BadRequest().body("Bad data"))
    } else {
        // choose Right variant
        Either::Right(Ok("Hello!"))
    }
}

提取器

类型安全的信息提取

Actix Web为类型安全的请求信息访问提供了一种称为提取器的工具(如impl FromRequest )。有很多内置的提取器实现(见implementors

提取器可以作为处理程序函数的参数访问。Actix Web支持每个处理函数最多12个提取器。参数的位置无关紧要。

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提供从请求路径提取的信息。路径中可提取的部分称为“动态段”,并用大括号标记。您可以反序列化路径中的任何可变段。

例如,对于注册为/users/{user_id}/{friend}路径的资源,可以反序列化两个段:user_id和friend。这些段可以按声明的顺序提取为元组

(如Path<(u32, String)>)

use actix_web::{get, web, App, HttpServer, 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(path: web::Path<(u32, String)>) -> Result<String> {
    let (user_id, friend) = path.into_inner();
    Ok(format!("Welcome {}, user_id {}!", friend, user_id))
}

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

通过匹配动态段名称和字段名称,也可以从serde提取路径信息到实现Deserialize trait 的类型。下面是一个使用serde而不是元组类型的等价示例。

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
}

作为一种非类型安全的选择,查询也是可以的(见 match_info docs)在处理程序中通过名称来请求路径参数:

#[get("/users/{user_id}/{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("user_id").parse().unwrap();

    Ok(format!("Welcome {}, user_id {}!", 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<T>类型为请求的查询参数提供提取功能。下面它使用serde urlencoded crate。

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

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

// this handler gets called if the query deserializes into `Info` successfully
// otherwise a 400 Bad Request error response is returned
#[get("/")]
async fn index(info: web::Query<Info>) -> String {
    format!("Welcome {}!", info.username)
}

Json

Json 许将请求体反序列化为结构体.要从请求体中提取有类型的信息,类型T必须实现serde::Deserialize.

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))
}

一些提取器提供了一种配置提取过程的方法。要配置提取器,需要将其配置对象传递给资源的.app data()方法。对于Json提取器,它返回一个JsonConfig。您可以配置JSON有效负载的最大大小以及自定义错误处理函数。

下面的示例将有效负载的大小限制为4kb,并使用自定义错误处理程序。

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
}

url编码的形式

可以将url编码的表单主体提取为一个结构体,就像Json<T>.该类型必须实现serde::Deserialize.

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。Example 以doc字符串形式提供。

actix_web::web::Bytes-您可以将请求的有效负载转换为Bytes。Example 以doc字符串形式提供。

Payload - 低级载荷提取器主要用于构造其他提取器。Example

应用程序状态提取器

应用程序状态可以通过web::Data提取器从处理器中访问;但是,state可以通过只读引用访问。如果需要对状态的可变访问,就必须实现它。

下面是一个存储已处理请求数量的处理器示例:

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

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

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<()> {
    let data = AppState {
        count: Cell::new(0),
    };

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

虽然这个处理程序将工作,data.count将只计算每个工作线程处理的请求数。要计算所有线程的请求总数,应该使用共享Arc和atomics.

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

#[derive(Clone)]
struct AppState {
    local_count: Cell<usize>,
    global_count: Arc<AtomicUsize>,
}

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

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

    let local_count = data.local_count.get();
    data.local_count.set(local_count + 1);

    format!(
        "global_count: {}\nlocal_count: {}",
        data.global_count.load(Ordering::Relaxed),
        data.local_count.get()
    )
}

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

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

注意:如果你想要整个状态被所有线程共享,如果你想要整个状态被所有线程共享,使用web::Data和app_data中描述的 共享可变状态.

在使用类似的阻塞同步原语时要小心Mutex或者RwLock在你的应用程序状态。Actix Web异步处理请求。如果处理程序中的临界区(critical section )太大或包含一个.await点,就会出现问题。如果这是一个问题,我们建议你读读Tokio’s advice on using blocking Mutex in async code.

进阶

Errors

actix web使用actix_web::error::Error类型和actix_web::error::ResponseError trait 来处理错误请求。

一个处理器返回一个Error(参考一般的Rust特征std::error:: error)在Result。它还实现了ResponseError trait,actix-web将该错误呈现为HTTP响应,并具有相应的actix_web:: HTTP::StatusCode。默认情况下会生成一个内部服务器错误:

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

一个Responder强制兼容的结果到HTTP响应:

impl<T: Responder, E: Into<Error>> Responder for Result<T, E>

Error上面的代码是actix-web的错误定义,和任何实现ResponseError的错误都可以自动转换为ResponseError。

Actix Web为一些常见的非Actix错误提供了ResponseError实现。

例如,如果处理程序以io::Error响应,该错误被转换为HttpInternalServerError:

use std::io;
use actix_files::NamedFile;

fn index(_req: HttpRequest) -> io::Result<NamedFile> {
    Ok(NamedFile::open("static/index.html")?)
}

有关ResponseError外部实现的完整列表,请参阅actix-web API文档( the actix-web API documentation )。

一个自定义错误响应的示例

下面是ResponseError的示例实现,使用声明性错误枚举的derive_more crate。

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(内部服务器错误),这就是上面执行索引处理程序时发生的情况。

覆盖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)
}

错误助手

Actix Web提供了一组错误帮助函数,这些函数对于从其他错误生成特定的HTTP错误代码非常有用。这里我们使用map_err将MyError转换为400(错误请求),它没有实现ResponseError trait:

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

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

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

    Ok(result.map_err(|e| error::ErrorBadRequest(e.name))?)
}

错误日志

Actix在WARN日志级别记录所有错误。如果将应用程序的日志级别设置为DEBUG,并且启用了RUST_BACKTRACE,则还会记录反向跟踪。它们可以通过环境变量进行配置:

>> RUST_BACKTRACE=1 RUST_LOG=actix_web=debug cargo run

Error类型使用原因的错误回溯(如果可用)。
如果底层失败没有提供反向跟踪,则会构造一个新的反向跟踪,指向发生转换的点(而不是错误的起源)。

错误处理的推荐实践

将应用程序产生的错误分为两大类可能会很有用:面向用户的错误和不面向用户的错误。

前者的一个例子是,我可以使用failure指定一个UserError enum,每当用户发送错误的输入时,它封装一个ValidationError返回:

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,
        }
    }
}

这将完全符合预期的行为,因为用display定义的错误消息是用用户读取的显式意图写入的。

然而,并不是所有错误都需要返回错误消息—在服务器环境中发生了许多失败,我们可能希望对用户隐藏细节。
例如,如果数据库停止工作,客户机库开始产生连接超时错误,或者HTML模板格式化不正确,在呈现时出现错误。
在这些情况下,最好将错误映射为适合用户使用的通用错误。

下面是一个将内部错误映射到面向用户的InternalError并带有自定义消息的示例:

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!")
}

通过将错误划分为面向用户的和不面向用户的,我们可以确保不会意外地向用户暴露应用程序内部抛出的错误,这些错误是用户不应该看到的。

错误记录

这是一个使用middleware::Logger的基本示例,它依赖于env_logger和log:

[dependencies]
env_logger = "0.8"
log = "0.4"
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
}

URL分发

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

请求处理程序是一个函数,它接受0个或多个可以从请求中提取的参数(例如,impl FromRequest),并返回一个可以转换为HttpResponse的类型(例如,impl Responder)。
更多信息可以在处理程序部分找到。

资源配置

资源配置是向应用程序添加新资源的行为。
资源有一个名称,它充当用于生成URL的标识符。
该名称还允许开发人员向现有资源添加路由。
一个资源也有一个模式,用来匹配URL的PATH部分(模式和端口后面的部分,例如URL http://localhost:8080/foo/bar?q=value中的/foo/bar)。
它不匹配QUERY部分(?后面的部分,例如http://localhost:8080/foo/bar?q=value中的q=value)。

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()提供了注册路由的简单方法,但要访问完整的资源配置,必须使用另一种方法。
application::service()方法向应用程序路由表添加单个资源。
这个方法接受一个路径模式、守卫和一个或多个路由。

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

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

pub fn main() {
    App::new()
        .service(web::resource("/prefix").to(index))
        .service(
            web::resource("/user/{name}")
                .name("user_detail")
                .guard(guard::Header("content-type", "application/json"))
                .route(web::get().to(HttpResponse::Ok))
                .route(web::put().to(HttpResponse::Ok)),
        );
}

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

配置一个路线

资源中包含一组路由。每个路由依次有一组守卫和一个处理程序。可以使用Resource::route()方法创建新的路由,该方法返回对新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报头,且该报头的值为text/plain,且path = /path,则返回HttpResponse::Ok()。

如果资源不能匹配任何路由,则返回“not FOUND”响应。

ResourceHandler::route()返回route对象。路由可以用类似构建器的模式配置。配置方法如下:

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

路线匹配

路由配置的主要目的是根据URL路径模式匹配(或不匹配)请求的路径。path表示被请求URL的路径部分。

actix-web实现这一点的方法非常简单。当请求进入系统时,对于系统中出现的每个资源配置声明,actix根据声明的模式检查请求的路径。这个检查是按照通过App::service()方法声明路由的顺序进行的。如果找不到,则使用默认资源作为匹配的资源。

当一个路由配置被声明时,它可能包含路由保护参数。
所有与路由声明相关的路由保护必须为真,以便在检查过程中为给定请求使用路由配置。如果在检查过程中,提供给路由配置的路由保护参数集中的任何一个参数返回false,那么该路由将被跳过,路由匹配将继续通过有序的路由集。

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

资源模式的语法

actix在pattern参数中使用的模式匹配语言的语法很简单。在路由配置中使用的模式可以以斜杠字符开始。如果模式不是以斜杠字符开头,则匹配时将在其前面加上隐式的斜杠。例如,以下模式是等价的:

{foo}/bar/baz

/{foo}/bar/baz

变量部分(替换标记)以{identifier}的形式指定,这意味着“接受下一个斜杠字符之前的任何字符,并将其用作HttpRequest.match_info()对象中的名称”。

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

match_info是Params对象,表示根据路由模式从URL中提取的动态部分。
它可以作为request.match_info使用。
例如,下面的模式定义了一个文字段(foo)和两个替换标记(baz和bar):

foo/{baz}/{bar}

上面的模式将匹配这些url,生成以下匹配信息

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

但是,它不会匹配以下模式:

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

对段中的段替换标记的匹配将只在模式中的段中的第一个非字母数字字符之前进行。因此,例如,如果使用这个路由模式:

foo/{name}.html

文本路径/foo/biz.html将匹配上面的路由模式,匹配结果将是Params {‘name’: ‘biz’}。

匹配结果将是Params {‘name’: ‘biz’}。匹配结果将是Params {‘name’: ‘biz’}。然而,文本路径/foo/biz将不匹配,因为它不包含一个文本.html在由{name}.html表示的段的结尾(它只包含biz,而不是biz.html)。

为了捕捉这两个片段,可以使用两个替换标记:

foo/{name}.{ext}

文本路径/foo/biz.html将匹配上述路由模式,匹配结果将是Params {’ name ': ’ biz ', ’ ext ': ’ html '}。出现这种情况是因为其中有一个字面部分。(句号)在两个替换标记{name}和{ext}之间。

替换标记可以选择指定一个正则表达式来决定一个路径段是否应该匹配该标记。要指定替换标记应该只匹配由正则表达式定义的特定字符集,必须使用略微扩展的替换标记语法。在大括号内,替换标记名称必须跟在冒号后面,然后直接跟在正则表达式后面。与替换标记[/]+相关联的默认正则表达式匹配一个或多个非斜杠的字符。例如,在底层,替换标记{foo}可以更详细地拼写为{foo:[/]+}。您可以将其更改为一个任意的正则表达式来匹配任意的字符序列,例如{foo:\d+}只匹配数字。

为了匹配段替换标记,段必须包含至少一个字符。
例如,对于URL /abc/:

  • /abc/{foo}将不匹配。
  • / {foo} /匹配。

注意:path将在匹配模式之前被url不加引号并解码为有效的unicode字符串,并且表示匹配路径段的值也将被url不加引号。

举个例子,下面这个模式:

foo/{bar}

当匹配以下URL时:

http://example.com/foo/La%20Pe%C3%B1a

匹配字典看起来是这样的(值是url解码的):

Params {'bar': 'La Pe\xf1a'}

路径段中的字面值字符串应该表示提供给actix的路径的解码值。
您不希望在模式中使用url编码的值。例如,与其这样:

/Foo%20Bar/{baz}

你可以这样使用:

/Foo Bar/{baz}

有可能得到“尾部匹配”。为此,必须使用自定义regex。

foo/{bar}/{tail:.*}

上面的模式将匹配这些url,生成以下匹配信息:

foo/1/2/           -> Params {'bar': '1', 'tail': '2/'}
foo/abc/def/a/b/c  -> Params {'bar': 'abc', 'tail': 'def/a/b/c'}

作用域路径

作用域帮助您组织共享公共根路径的路由。可以在作用域中嵌套作用域。

假设您想组织用于查看“Users”的端点的路径。这些途径可能包括:

  • /users
  • /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
}

作用域路径可以包含可变路径段作为资源。与未限定范围的路径一致。

你可以从HttpRequest::match_info()中获取变量路径段。
路径提取器还能够提取范围级变量段。

匹配信息

所有表示匹配路径段的值都可以在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和v2的值将被解析为" 1 “和” 2 "。

路径信息提取器

Actix提供了类型安全路径信息提取的功能。路径提取信息,目标类型可以用几种不同的形式定义。最简单的方法是使用元组类型。元组中的每个元素必须对应于路径模式中的一个元素。
也就是说,你可以匹配路径模式/{id}/{username}/来匹配path <(u32, String)>类型,但是path <(String, 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
}

还可以将路径模式信息提取到一个结构。在这种情况下,必须实现这个结构 serde 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为请求查询参数提供了类似的功能。

生成资源的url

使用HttpRequest.url_for()方法根据资源模式生成url。
例如,如果你配置了一个名为“foo”的资源,模式为“{a}/{b}/{c}”,你可以这样做:

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://example.com/test/1/2/3的字符串(至少如果当前协议和主机名隐含了http://example.com)。
url_for()方法返回Url对象,这样您就可以修改这个Url(添加查询参数、锚等)。Url_for()只能对已命名的资源调用,否则将返回错误。

外部资源

资源是有效的url,可以注册为外部资源。它们只用于URL生成目的,不会考虑在请求时进行匹配。

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

#[get("/")]
async fn index(req: HttpRequest) -> impl Responder {
    let url = req.url_for("youtube", &["oHg5SJYRHA0"]).unwrap();
    assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0");

    url.to_string()
}

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

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

通过规范化它意味着:

  • 在路径末尾添加一个斜杠。
  • 用一个斜杠替换多个斜杠。

一旦找到正确解析的路径,处理程序就返回。
如果所有条件都启用,则归一化条件的顺序为: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数据。

可以只对GET请求注册路径规范化:

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
}

使用应用前缀组合应用

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。

自定义路由保护

您可以将守卫看作一个简单的函数,它接受请求对象引用并返回true或false。
形式上,守卫是任何实现Guard trait.的对象。
Actix提供了几个谓词,您可以查看API文档中的函数functions section部分。

这里有一个简单的保护程序,它检查请求是否包含特定的头信息

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报头时,才会调用索引处理程序。

守卫不能访问或修改请求对象,但可以在请求扩展request extensions.中存储额外的信息。

修改保护值

您可以通过将任何谓词值包装在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守卫接受守卫列表,如果提供的任何守卫匹配,则进行匹配。即:

guard::Any(guard::Get()).or(guard::Post())

All guard接受一个警卫列表,如果提供的所有警卫都匹配,则匹配。即:

guard::All(guard::Get()).and(guard::Header("content-type", "plain/text"))

更改默认的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
}

请求

JSON请求

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

第一种选择是使用Json提取器。首先,定义一个处理程序函数,它接受Json作为参数,然后,使用.to()方法注册这个处理程序。
通过使用serde_json::Value作为类型T,也可以接受任意有效的json对象。

json请求中的json的第一个例子依赖于serde:

[dependencies]
serde = { version = "1.0", features = ["derive"] }

JSON请求的第二个例子依赖于serde和serde_json和futures:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1"
futures = "0.3"

如果需要为某个字段添加默认值,请参考serde documentation.

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
}

您还可以手动将有效负载加载到内存中,然后反序列化它。

在下面的例子中,我们将反序列化一个MyObj结构体。
我们需要首先加载请求体,然后将json反序列化为一个对象。

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
}

在examples目录中可以找到这两个选项的完整示例examples directory

内容编码

Actix Web会自动解压有效负载。支持以下编解码器:

  • Brotli
  • Gzip
  • Deflate
  • Zstd

如果请求报头包含Content-Encoding报头,则根据报头值对请求负载进行解压缩。不支持多种编解码器同时存在,例如:Content-Encoding: br, gzip。

分块传输编码

Actix自动解码分块编码。有web::Payload提取器已经包含了解码的字节流。如果使用支持的压缩编解码器之一(br、gzip、deflate)压缩请求负载,则对字节流进行解压缩。

多部分的body

Actix Web通过一个外部crate提供多部分流支持,actix-multipart.

在examples目录中可以找到完整的示例 examples directory.。

Urlencoded body

Actix Web通过解析反序列化实例的Web::Form提取器为application/x-www-form-urlencoded encoded body提供了支持。实例的类型必须从serde实现Deserialize trait。

UrlEncoded future在以下几种情况下可以解析为错误:

  • 内容类型不是application/x-www-form-urlencoded
  • 传输编码是分块的。
  • Content-length大于256k
  • payload terminates with error.
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))
}

流请求

HttpRequest是一个Bytes对象流。它可以用来读取请求体负载。

在下面的例子中,我们一个块一个块地读取和打印请求负载:

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())
}

响应

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

查看文档中 documentation 的类型说明。

方法.body、.finish和.json完成响应的创建并返回一个构造好的HttpResponse实例。如果在同一个构建器实例上多次调用此方法,则构建器将出现混乱。

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

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

JSON Response

Json类型允许使用格式良好的Json数据进行响应:只需返回类型为Json的值,其中T是要序列化为Json的结构类型。类型T必须从serde实现Serialize trait。

为了让下面的例子工作,你需要在Cargo.toml中添加serde到你的依赖项中:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
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
}

以这种方式使用Json类型,而不是在HttpResponse上调用. Json方法,可以立即清楚地看到该函数返回Json,而不是任何其他类型的响应。

内容编码

Actix Web可以使用压缩中间件自动压缩 Compress middleware.payload。支持以下编解码器:

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

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

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

middleware::BodyEncoding trait.的编码参数对响应有效载荷进行压缩。默认情况下,使用ContentEncoding::Auto。如果选择了ContentEncoding::Auto,则压缩依赖于请求的Accept-Encoding头。

ContentEncoding::Identity可用于禁用压缩。如果选择了另一种内容编码,则对该编解码器强制压缩。

例如,要为单个处理器启用brotli,请使ContentEncoding::Br:

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

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

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

或者对于整个应用程序

use actix_web::{http::ContentEncoding, dev::BodyEncoding, HttpResponse};

async fn index_br() -> HttpResponse {
    HttpResponse::Ok().body("data")
}

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

    HttpServer::new(|| {
        App::new()
            .wrap(middleware::Compress::new(ContentEncoding::Br))
            .route("/", web::get().to(index_br))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

在本例中,我们通过将内容编码设置为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
}

当处理已经压缩的正文时(例如服务资产时),将内容编码设置为Identity以避免压缩已经压缩的数据,并手动设置content-encoding头:

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)
}

也可以在应用程序级别设置默认的内容编码,默认使用ContentEncoding::Auto,这意味着自动的内容压缩协商。

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
}

测试

每个应用程序都应该经过良好的测试。Actix Web提供了执行单元测试和集成测试的工具。

单元测试

对于单元测试,actix-web提供了一个请求构建器类型。TestRequest实现了一个类似构建器的模式。您可以使用to_http_request()生成一个HttpRequest实例,并使用它调用处理程序。

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

综合测试

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

可以使用TestRequest::get(), TestRequest::post()等方法向测试服务器发送请求。

要创建一个用于测试的Service,可以使用test::init_service方法,它接受一个常规的应用构建器。

有关更多信息,请查看API文档。API documentation

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

    use super::*;

    #[actix_web::test]
    async fn test_index_get() {
        let app = test::init_service(App::new().route("/", web::get().to(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().route("/", web::get().to(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 }))
                .route("/", web::get().to(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);
    }
}

流响应测试

如果您需要测试流生成,调用take_body()并将产生的ResponseBody转换为未来的ResponseBody并执行它就足够了,例如在测试Server Sent Events时。

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")
        );
    }
}

中间件

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

中间件还可以与响应处理挂钩。

通常,中间件涉及以下操作:

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

中间件为每个App, scope`, Resource注册,并按相反的顺序执行。通常,中间件是一种实现Service traitTransform trait. 的类型。traits中的每个方法都有一个默认实现。每个方法可以立即返回一个结果,也可以返回一个未来的对象。

下面演示了如何创建一个简单的中间件:

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!" }),
        );
}

Actix Web提供了一些有用的中间件,如日志、用户会话、压缩等。

警告:如果多次使用wrap()或wrap_fn(),最后一次出现的将首先执行。

日志

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

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

用法

使用指定的格式创建Logger中间件。Default Logger可以用Default方法创建,它使用默认格式:

 %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

格式

  • %% The percent sign
  • %a Remote IP-address (IP-address of proxy if using reverse proxy)
  • %t Time when the request was started to process
  • %P The process ID of the child that serviced the request
  • %r First line of request
  • %s Response status code
  • %b Size of response in bytes, including HTTP headers
  • %T Time taken to serve the request, in seconds with floating fraction in .06f format
  • %D Time taken to serve the request, in milliseconds
  • %{FOO}i request.headers[‘FOO’]
  • %{FOO}o response.headers[‘FOO’]
  • %{FOO}e os.environ[‘FOO’]

默认响应头

要设置默认的响应头,可以使用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
}

Actix Web为会话管理提供了一个通用的解决方案。
actix-session 中间件可以使用多种后端类型来存储会话数据。

默认情况下,只启用cookie会话后端功能。可以添加其他后端实现。

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

cookie可以有签名或私有的安全策略。每个都有一个各自的CookieSession构造函数。

客户端可以查看签名的cookie,但不能修改。私有cookie既不能被客户端查看也不能被修改。

构造函数接受一个键作为参数。这是cookie会话的私钥——当修改此值时,所有会话数据将丢失。

通常,你创建一个SessionStorage中间件,并用特定的后端实现初始化它,比如CookieSession。要访问会话数据,必须使用Session 提取器。此方法返回一个Session对象,该对象允许我们获取或设置会话数据。

use actix_session::{CookieSession, Session};
use actix_web::{web, App, Error, HttpResponse, HttpServer};

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(
                CookieSession::signed(&[0; 32]) // <- create cookie based session middleware
                    .secure(false),
            )
            .service(web::resource("/").to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

错误处理器

ErrorHandlers中间件允许我们为响应提供定制的处理程序。

可以使用ErrorHandlers::handler()方法为特定的状态码注册自定义错误处理程序。您可以修改现有响应,也可以创建一个全新的响应。错误处理程序可以立即返回响应,也可以返回解析为响应的未来响应。

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
}

单个文件

可以使用自定义路径模式和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
}

文件夹

要服务特定目录和子目录中的文件,可以使用Files。
文件必须用App::service()方法注册,否则它将无法为子路径服务。

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

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

配置

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

set_content_disposition-函数用于映射文件的mime到相应的Content-Disposition类型

use_etag-指定是否计算ETag并包含在头文件中。

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支持WebSockets actix-web-actors crate.可以使用web::Payload 将请求的Payload转换为 ws::Message 流,然后使用流组合器来处理实际的消息,但是用http actor来处理websocket通信更简单。

下面是一个简单的websocket回显服务器的例子:

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
}

一个简单的websocket回显服务器示例可以在examples directory.中找到。

一个能够通过websocket或TCP连接进行聊天的聊天服务器示例可以在websocket-chat directory目录中找到

HTTP/2

Actix Web将首选HTTP/2连接,如果客户端信号支持它通过TLS ALPN.

转发

当其中一个rusls或openssl特性被启用时,HttpServer会分别提供bind_russtls方法和bind_openssl方法。

[dependencies]
actix-web = { version = "4", features = ["openssl"] }
openssl = { version = "0.10", features = ["v110"] }
use actix_web::{web, App, HttpRequest, HttpServer, Responder};
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};

async fn index(_req: HttpRequest) -> impl Responder {
    "Hello."
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    // load TLS keys
    // to create a self-signed temporary cert for testing:
    // `openssl req -x509 -newkey rsa:4096 -nodes -keyout key.pem -out cert.pem -days 365 -subj '/CN=localhost'`
    let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
    builder
        .set_private_key_file("key.pem", SslFiletype::PEM)
        .unwrap();
    builder.set_certificate_chain_file("cert.pem").unwrap();

    HttpServer::new(|| App::new().route("/", web::get().to(index)))
        .bind_openssl("127.0.0.1:8080", builder)?
        .run()
        .await
}

不支持RFC 7540§3.2中描述的HTTP/2升级。
基于先验知识启动HTTP/2支持明文和TLS连接(RFC 7540§3.4)(当使用较低级别的actix-http服务构建器时)。

查看具体的TLS示例the TLS examples

模式

Auto-Reloading开发服务器

在开发过程中,自动重新编译代码是非常方便的。 这可以很容易地通过使用cargo-watch.来完成

cargo watch -x 'run --bin app'

历史上的注意

这个页面的旧版本建议使用systemfd和listenfd的组合,但是这有很多问题,很难正确地集成,特别是当它是更广泛的开发工作流的一部分时。

我们认为在自动装填时,cargo-watch已经足够了。

数据库

我们有几个示例项目展示了异步数据库适配器的使用:

  • Postgres: https://github.com/actix/examples/tree/master/databases/postgres
  • SQLite: https://github.com/actix/examples/tree/master/databases/sqlite
  • MongoDB: https://github.com/actix/examples/tree/master/databases/mongodb

Diesel

当前版本的Diesel (v1)不支持异步操作,因此,使用web::block 函数将数据库操作转移到Actix运行时线程池是很重要的。

您可以创建操作函数,对应于您的应用程序将在数据库上执行的所有操作。

fn insert_new_user(db: &SqliteConnection, user: CreateUser) -> Result<User, Error> {
    use self::schema::users::dsl::*;

    // Create insertion model
    let uuid = format!("{}", uuid::Uuid::new_v4());
    let new_user = models::NewUser {
        id: &uuid,
        name: &user.name,
    };

    // normal diesel operations
    diesel::insert_into(users)
        .values(&new_user)
        .execute(&self.0)
        .expect("Error inserting person");

    let mut items = users
        .filter(id.eq(&uuid))
        .load::<models::User>(&self.0)
        .expect("Error loading person");

    Ok(items.pop().unwrap())
}

现在应该使用r2d2这样的 crate 来设置数据库池,这意味着多个处理程序可以同时操作DB,并且仍然接受新的连接。简单地说,池在你的应用程序状态。(在这种情况下,最好不要使用状态包装器结构,因为池为您处理共享访问。)

type DbPool = r2d2::Pool<ConnectionManager<SqliteConnection>>;

#[actix_web::main]
async fn main() -> io::Result<()> {
    // Create connection pool
    let pool = r2d2::Pool::builder()
        .build(manager)
        .expect("Failed to create pool.");

    // Start HTTP server
    HttpServer::new(move || {
        App::new().app_data(web::Data::new(pool.clone()))
            .resource("/{name}", web::get().to(index))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

现在,在请求处理程序中,使用Data<T>提取器从应用程序状态获取池并从中获取连接。这提供了一个拥有的数据库连接,可以传递到web::block 闭包。然后只需要调用action函数和必要的参数,.await结果。

这个例子还映射错误到HttpResponse之前使用?
操作符,但如果返回错误类型实现了ResponseError,则没有必要这样做。

async fn index(pool: web::Data<DbPool>, name: web::Path<(String)>) -> impl Responder {
    let name = name.into_inner();

    let conn = pool.get().expect("couldn't get db connection from pool");

    let user = web::block(move || actions::insert_new_user(&conn, &user))
        .await
        .map_err(|e| {
            eprintln!("{}", e);
            HttpResponse::InternalServerError().finish()
        })?;
    
    Ok(HttpResponse::Ok().json(user))
}

完整示例

https://github.com/actix/examples/tree/master/databases/diesel

示意图

HTTP服务器初始化

体系结构概述

下面是HttpServer初始化的关系图,它在以下代码中发生

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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7Ur7ux6v-1655459163984)(https://actix.rs/img/diagrams/http_server.svg)]

连接的生命周期

体系结构概述

在Server开始监听所有套接字之后,AcceptWorker是两个主要的循环,负责处理传入的客户端连接。

一旦连接被接受,应用程序级协议处理将在Worker.派生的特定于协议的Dispatcher循环中发生。

Please note, below diagrams are outlining happy-path scenarios only.

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WHfWiqiv-1655459163987)(https://actix.rs/img/diagrams/connection_overview.svg)]

更详细地接收循环

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-B2qjeZzc-1655459163987)(https://actix.rs/img/diagrams/connection_accept.svg)]

大多数代码实现驻留在 actix-server crate中,用于结构Accept.

工作循环的更多细节

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cmHcbWbG-1655459163988)(https://actix.rs/img/diagrams/connection_worker.svg)]

大多数代码实现驻留在struct Worker.的actix-server crate中。

请求循环概况

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-9eWq2zKu-1655459163989)(https://actix.rs/img/diagrams/connection_request.svg)]

请求循环的大部分代码实现驻留在actix-webactix-http crates中。

API文档链接

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值