Rust web开发 ActixWeb 入门

xuejianxinokok 2024-06-10 14:33:03 阅读 87

Rust Actix Web 入门

在这里插入图片描述

目前,Actix Web 仍然是 Rust Web 后端生态系统中极其强大的竞争对手。尽管之前的事件可能对其产生影响,但它仍然很强大,并且是 Rust 中最受推荐的 Web 框架之一。最初基于同名的 actor 框架 ( actix ),后来它已经消失,并且 actix 现在仅用于 websocket 端点。

本文将主要讨论 v4.4。

Actix Web 入门

首先,使用 cargo init example-api 生成项目,将 cd 到该文件夹中,然后使用以下命令将 actix-web crate 添加到项目中:

cargo add actix-web

现在已经准备好开始了!如果想复制样板文件以直接编写您的应用程序,如下所示:

use actix_web::{ web, App, HttpServer, Responder};#[get("/")]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("/") // ...so this handles requests for `GET /app/index.html` .route("/", web::get().to(index)), ) }) .bind(("127.0.0.1", 8080))? .run() .await}

Actix Web 中的路由

使用 Actix Web 时,大多数返回 实现actix_web::Responder trait 的函数都可以用作路由。请参阅下面的基本 Hello World 示例:

#[get("/")]async fn index() -> impl Responder { "Hello world!"}

然后可以将此处理函数输入到 actix_web::App 中,然后将其作为参数传递给 HttpServer

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

现在,每当您访问 /index.html 时,它都应该返回“Hello world!”。但是,如果您想创建多个 子路由 类型,然后最后将它们全部合并到应用程序中,您可能会发现这种方法有点缺乏。在这种情况下,您将需要 ServiceConfig 类型,您也可以这样写:

use actix_web::{ web, App, HttpResponse};// this function could be located in different modulefn config(cfg: &mut web::ServiceConfig) { cfg.service(web::resource("/test") .route(web::get().to(|| HttpResponse::Ok())) .route(web::head().to(|| HttpResponse::MethodNotAllowed())) );}#[actix_web::main]async fn main() -> std::io::Result<()> { HttpServer::new(|| { App::new().configure(config) }) .bind(("127.0.0.1", 8080))? .run() .await}

Actix Web 中的提取器(Extractor)正是这样的:实现类型安全的请求,当传递到处理函数时,将尝试从处理函数的请求中提取相关数据。例如, actix_web::web::Json 提取器将尝试从请求正文中提取 JSON。然而,要成功反序列化 JSON,您需要使用 serde 包 - 最好与 derive 函数一起使用,为您的结构添加自动反序列化和序列化派生宏。您可以通过执行以下命令来安装 serde:

cargo add serde -F derive

现在可以将其用作派生宏,如下所示:

use actix_web::web;use serde::Deserialize;#[derive(Deserialize)]struct Info { username: String,}// deserialize `Info` from request's body#[post("/submit")]async fn submit(info: web::Json<Info>) -> String { format!("Welcome {}!", info.username)}

Actix Web 还支持 paths, queries and forms。您还需要在此处使用 serde - 尽管对于paths,您还需要使用 Actix Web 路由宏来声明路径参数的确切位置。我们可以在下面找到所有这 3 种方法的示例:

#[derive(Deserialize)]struct Info { username: String,}// extract path info using serde#[get("/users/{username}")] // <- define path parametersasync fn index(info: web::Path<Info>) -> String { format!("Welcome {}!", info.username)}// data is passed in here through query parameters in the URL// for example, google.com/?hello=world#[get("/")]async fn index(info: web::Query<Info>) -> String { format!("Welcome {}!", info.username)}// data is passed into the Form extractor through a HTML Form element#[post("/")]async fn index(form: web::Form<Info>) -> actix_web::Result<String> { Ok(format!("Welcome {}!", form.username))}

有兴趣编写自己的提取器吗?你可以做到的!编写自己的提取器只需要实现 FromRequest 特征。查看 HTTP 标头提取器的这段代码,它向您展示了它的具体工作原理:

use actix_web::dev::Payload;use actix_web::{ FromRequest, http::header::Header as ParseHeader, HttpRequest, error::ParseError};#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)]pub struct Header<T>(pub T);impl<T> FromRequest for Header<T>where T: ParseHeader,{ type Error = ParseError; type Future = Ready<Result<Self, Self::Error>>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match T::parse(req) { Ok(header) => ok(Header(header)), Err(e) => err(e), } }}

请注意, T: ParseHeader trait 约束 特定于此特征实现,因为为了使header有效,它需要能够成功解析为header,并在实现 actix_web::error::Error 。虽然我们也有 extract 作为提供的方法,但 from_request 是这里唯一需要实现的方法,它返回 Self::Future 。也就是说,我们需要返回一个 已准备好等待的结果(ready to be awaited) - 您可以在此处找到有关 Ready 结构的更多信息。其他提取器(例如 JSON 提取器)也允许您更改其配置 - 您可以在此文档页面中找到有关它的更多信息。

一般来说,responses只需要实现 actix_web::Responder trait就能够响应。尽管在实际响应类型方面已经有广泛的实现,因此您通常不需要实现自己的类型,但在某些特定的用例中这可能会很有帮助;例如,能够记录您的应用程序可能具有的所有类型的响应。

连接到数据库

通常,在设置数据库时,您可能需要设置数据库连接:

use sqlx::PgPoolOptions;#[actix_web::main]async fn main() -> std::io::Result<()> { let dbconnection = PgPoolOptions::new() .max_connections(5) .connect(r#"<db-connection-string-here>"#).await; //... rest of your code}

然后,您需要配置自己的 Postgres 实例,无论是本地安装在计算机上、通过 Docker 还是其他方式配置。但是,使用 Shuttle,我们可以消除这种情况,因为运行时会为您配置数据库:

use actix_web::{ get, web::ServiceConfig};use shuttle_actix_web::ShuttleActixWeb;#[shuttle_runtime::main]async fn actixweb( #[shuttle_shared_db::Postgres] pool: PgPool,) -> ShuttleActixWeb<impl FnOnce(&mut ServiceConfig) + Send + Clone + 'static> { let state = AppState { pool }; // .. the rest of your code}

在本地,这是通过 Docker 完成的,但在部署中,有一个总体流程可以为您完成此操作!不需要额外的工作。我们还有一个 AWS RDS 数据库产品,需要零 AWS 知识才能设置 - 请访问此处了解更多信息。

Actix Web 中的应用程序状态

路由 非常棒(连接数据库也非常容易!),但是当您需要 存储变量时,您可能想要寻找可以在应用程序中存储和使用它们的东西。这就是共享可变状态的用武之地:您在跨整个应用程序构建服务时声明它,然后您可以将它用作处理程序函数中的提取器。例如,您可能需要共享数据库池、计数器或 Websocket 订阅者的共享hashmap。您可以像这样声明和使用状态:

use sqlx::PgPool;#[derive(Clone)]struct AppState { db: PgPool}#[get("/")]async fn index(data: web::Data<AppState>) -> String { let res = sqlx::query("SELECT 'Hello World!'").fetch_all(&data.db).await.unwrap(); format!("{res}")}

然后你可以像这样实现它:

#[actix_web::main]async fn main() -> std::io::Result<()> { let db = connect_to_db(); let state = web::Data::new(AppState { db }); HttpServer::new(move || { // move app state into the closure App::new() .app_data(state.clone()) // <- register the created data .route("/", web::get().to(index)) }) .bind(("127.0.0.1", 8080))? .run() .await}

Actix Web 中的中间件

在 Actix Web 中,中间件用作能够向(一组)路由添加通用功能的介入层,方法是在处理程序函数运行之前获取请求,执行一些操作,运行实际的处理程序函数本身,然后中间件进行额外的处理(如果需要)。默认情况下,Actix Web 有几个我们可以使用的默认中间件,包括日志记录、路径规范化、访问外部服务和修改应用程序状态(通过 ServiceRequest 类型)。

请参阅下面的示例,了解如何实现默认 Logger 中间件:

use actix_web::{ middleware::Logger, App};#[actix_web::main]async fn main() -> std::io::Result<()> { // access logs are printed with the INFO level so ensure it is enabled by default env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); let app = App::new() .wrap(Logger::default()); // ... rest of your code}

此外,您还可以在 Actix Web 中编写自己的中间件!对于许多用例,我们可以使用 crate actix-web-lab 中方便的 middleware::from_fn 帮助器(在即将发布的版本中将提升为 actix-web 本身)。例如,在请求处理流程的不同部分打印消息,如下所示:

use actix_web::{ body::MessageBody, dev::{ ServiceRequest, ServiceResponse}};use actix_web_lab::middleware::{ from_fn, Next};async fn print_before_and_after_handler( req: ServiceRequest, next: Next<impl MessageBody>,) -> Result<ServiceResponse<impl MessageBody>, Error> { println!("Hi from start. You requested: {}", req.path()); let res = next.call(req).await?; println!("Hi from response"); Ok(res)}let app = App::new() .wrap(from_fn(print_before_and_after_handler)) .route( "/", web::get().to(|| async { "Hello from handler!" }),);

为了能够编写更复杂的中间件,我们实际上需要实现两个traits - Service<Req> 用于实现实际的中间件本身以及 Transform<S, Req> 这是构建器所需的处理请求的实际服务(就我们构建服务而言,我们实际上将使用构建器,而不是中间件!外部服务将在检测到 HTTP 请求时自动调用中间件)。

对于实际的中间件实现,让我们看一下编写一个仅打印消息的中间件。我们可以通过实现 Transform 特征来创建中间件的构建器:

use std::{ future::{ ready, Ready, Future}, pin::Pin};use actix_web::{ dev::{ forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, web, Error,};pub struct SayHi;// `S` - type of the next service// `B` - type of response's bodyimpl<S, B> Transform<S, ServiceRequest> for SayHiwhere S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S::Future: 'static, B: 'static,{ // setting up the types for the middleware to work type Response = ServiceResponse<B>; type Error = Error; type InitError = (); type Transform = SayHiMiddleware<S>; type Future = Ready<Result<Self::Transform, Self::InitError>>; // this immediately returns the middleware fn new_transform(&self, service: S) -> Self::Future { ready(Ok(SayHiMiddleware { service })) }}

现在我们可以编写中间件本身了!在内部,中间件必须实现一个泛型类型 - 然后在 Service 特征中声明。请注意,我们手动重新实现了 futures_util 中名为 LocalBoxFuture 的类型 - 也就是说,未来不需要 Send 特征并且是安全的使用它是因为它在解除引用时实现了 Unpin ,这会自动取消任何先前的线程安全保证。

pub struct SayHiMiddleware<S> { /// The next service to call service: S,}// This future doesn't have the requirement of being `Send`.// See: futures_util::future::LocalBoxFuturetype LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;// `S`: type of the wrapped service// `B`: type of the body - try to be generic over the body where possibleimpl<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<Result<Self::Response, Self::Error>>; // This service is ready when its next service is ready forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { println!("Hi from start. You requested: {}", req.path()); // A more complex middleware, could return an error or an early response here. // we do not immediately await this, which means nothing happens // this future gets moved into a Box let fut = self.service.call(req); Box::pin(async move { // this future gets awaited now let res = fut.await?; // we can now do any work we need to after the request println!("Hi from response"); Ok(res) }) }}

现在已经编写了中间件,现在可以将其添加到应用程序中:

#[actix_web::main]async fn main() -> std::io::Result<()> { let app = App::new() .wrap(SayHi); // ... rest of your code}

Actix Web 中的静态文件

Actix Web 中简单、简洁的静态文件服务是通过 actix_files 箱完成的 - 要添加它,只需通过 Cargo 添加它,如下所示:

cargo add actix-files

设置静态文件服务的路由如下所示:

use actix_files::NamedFile;use actix_web::{ HttpRequest, Result};use std::path::PathBuf;#[get("/")]async fn index(req: HttpRequest) -> Result<NamedFile> { let path: PathBuf = req.match_info().query("filename").parse().unwrap(); Ok(NamedFile::open(path)?)}

该路由允许我们提供任何可以找到并与文件名匹配的文件 - 例如,如果我们有一个为该路由提供服务的基本路由,如果我们然后运行我们的应用程序并转到 /index.html ,则该路由将尝试在项目根目录中查找名为 index.html 的文件。

请注意,您在任何情况下都不应该尝试使用带有 .* 的路径尾部来返回 NamedFile - 这会产生严重的安全隐患,并且会打开您的 Web 服务以进行路径遍历!这在 Actix Web 文档中进行了记录,您可以在此处找到有关路径遍历攻击的更多信息。为了防止这种情况,您可以尝试使用 std::fs::canoncalize 来尝试验证路径文件是否正确或未尝试遍历目标文件夹之外。

然而,当您需要提供多个文件时,这有点笨拙 - 例如,特别是如果您需要提供 HTML 文件的文件夹。为了能够从您的网络服务提供文件文件夹,最好的方法是使用 actix_files::Files 并将其附加到您的 App

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", ".") .use_last_modified(true), ) }) .bind(("127.0.0.1", 8080))? .run() .await}

请注意,您还可以使用多个选项来扩展 Files 结构,例如在基本路径上显示文件目录本身(用于文件服务)并允许隐藏文件,您可以在此处找到更多信息。

此外,我们还可以使用 askama 的 HTML 模板功能来增强我们的 HTML 文件服务!我们可以像这样开始:

cargo add askama askama-actix-web -F askama/with-actix-web

这会添加 askama 本身以及 askama::Template 类型的 Responder 特征实现。 askama 期望您的文件默认位于项目根目录的名为 templates 的子文件夹中,因此让我们创建该文件夹,然后使用以下内容创建一个 index.html 文件HTML 代码位于:

Hello, { {name}}!

要在我们的应用程序中使用 Askama,我们需要声明一个使用 Template 派生宏的结构体,并使用 Askama 的 template 宏将该结构体指向我们希望它使用的文件:

#[derive(Template)]#[template(path = "index.html")]struct IndexTemplate<'a> { name: &'a str}#[get("/")]async fn index_route() -> impl Responder { IndexTemplate { name: "Shuttle" }}

然后我们可以将其添加为我们的 Actix Web 服务中的常规处理程序函数,我们就可以开始了!当您转到返回 index_route 的路径时,您应该看到“Hello, Shuttle!”作为 HTML 响应。

部署

一般来说,由于必须使用 Dockerfile,使用 Rust 后端程序进行部署可能不太理想,尽管如果您已经有使用 Docker 的经验,这对您来说可能不是一个问题 - 特别是如果您使用 cargo-chef .但是,如果您使用的是 Shuttle,则只需使用 cargo shuttle deploy 即可完成。无需设置。

尾声

谢谢阅读! Actix Web 是一个强大的框架,您可以用它来增强您的 Rust 产品组合,如果您想构建您的第一个 Rust API,那么它也是一个深入了解 Rust 的绝佳框架。


原文地址:Getting Started with Actix Web in Rust



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。