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 module fn 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 parameters async 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 body impl<S, B> Transform<S, ServiceRequest> for SayHi where 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::LocalBoxFuture type 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 possible 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<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 的绝佳框架。