RustWeb开发之ActixWeb入门

安全运维得看我 2023-12-30 07:36:17

随着Rust语言的流行,很多人已经了解到Rust是一个安全高效的系统语言。但是你不知道的是,Rust也是一门优秀的Web开发语言,适合各种Web系统、网站、微服务、API接口开发等任务。

今天我们就介绍一个Rust的Web框架Actix Web来证明Rust的Web开发也不是什么难事。

概述

Actix Web最初来源于其同名的Actor框架,目前Actix已经不咋流行,只用于于websocket。Actix Web则发展壮大成了Rust Web后端生态系统中最受欢迎的框架之一。

由于天生来自于actor的基因,Actix web框架有actor的各种优势,支持高并发、高性能、高可靠性的 Web 应用程序开发体验。

入门

首先,需要使用cargo init example-api生成项目,cd进入文件夹,然后使用以下命令添加actix-web打包到项目中:

cargo add actix-web

至此,你的准备工作已经完成。

我们先来复制一个模板文件以直接来修改一个“Hello Chongchong”Web应用作为第起点:

use actix_web::{web, App, HttpServer, Responder};#[get("/")]async fn index() -> impl Responder {"Hello Chongchong!"}#[actix_web::main]async fn main() -> std::io::Result<()> {HttpServer::new(|| {App::new().service(web::scope("/").route("/", web::get().to(index)),)}).bind(("127.0.0.1", 8080))?.run().await}

路由

使用Web框架的第一步就是撰写路由,Actix Web当然也是如此。大多数actix_web::Responder返回特征可以路由。例如“Hello Chongchong”示例中的:

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

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

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

这样每次访问/index.html,就会返回“Hello Chongchong!”。 但是,如果想创建多个迷你路由器类型,然后最后将它们全部合并到应用程序中,可能会发现这种方法太繁琐了。

为了处理他们,需要ServiceConfig:

use actix_web::{web, App, HttpResponse};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中的提取器正是这样的:类型安全的请求实现,当传递到处理函数时,将尝试从处理函数的请求中提取相关数据。例如, actix_web::web::Jsonextractor将尝试从请求正文中提取JSON。然而,要成功反序列化JSON,需要使用serde板条箱,同时也使用derive为我们的结构添加自动反序列化和序列化派生宏的函数。可以通过执行以下命令来安装serde:

cargo add serde -F derive

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

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

Actix Web还支持路径、查询和表单。也需要使用serde。尽管使用路径,还需要使用Actix Web路由宏来声明路径参数的确切位置。示例:

#[derive(Deserialize)]struct Info {username: String,}//使用serde提取路径#[get("/users/{username}")] // <- 定义路径参数async fn index(info: web::Path<Info>) -> String {format!("Welcome {}!", info.username)}// 数据通过URL的的请求参数传递,例如aa/?hello=chongchong#[get("/")]async fn index(info: web::Query<Info>) -> String {format!("Welcome {}!", info.username)}// 使用一个HTML表单元素将数据传递表单提取器(Form extractor)#[post("/")]async fn index(form: web::Form<Info>) -> actix_web::Result<String> {Ok(format!("Welcome {}!", form.username))}

表单提取器也容易,下面是一个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>whereT: 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特征绑定特定于此特征实现,因为为了使标头成为有效标头,它需要能够成功解析为标头,并实现错误actix_web::error::Error。虽然也可以用extract方法,from_request是这里唯一需要实现的方法,它返回 Self::Future。 也就是说,需要返回一个已准备好等待的结果。一般来说,响应只需要实现actix_web::Responder能够做出反应的特质。尽管在实际响应类型方面已经有广泛的实现 ,因此通常不需要实现自己的类型,但在某些特定的用例中这可能会很有帮助;例如,能够记录应用程序可能具有的所有类型的响应。

数据库

不连数据库的Web不是真正的动态Web,actix_web中使用数据库,首先要设置数据库,加载库驱动,配置数据库连接,比如连一个Postgres的典型代码:

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;//其他代码}

然后,需要配置自己的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 };//其他代码}

如果你没有Postgres实例,也不知道怎么部署,最简单方法就是拉一个Docker容器,然后启动就成。

应用程序状态

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

use sqlx::PgPool;#[derive(Clone)]struct AppState {db: PgPool}#[get("/")]async fn index(data: web::Data<AppState>) -> String {let res = sqlx::query("SELECT 'Hello Chongchong!'").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 || {// 把应用状态转移到闭包App::new().app_data(state.clone()) // <- 注册生成的数据.route("/", web::get().to(index))}).bind(("127.0.0.1", 8080))?.run().await}中间件

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

下面代码实现了默认日志记录中间件:

use actix_web::{middleware::Logger, App};#[actix_web::main]async fn main() -> std::io::Result<()> {// INFO类型的访问日志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支持编写DIY自己的中间件。对于许多用例,可以使用方便的中间件助手actix-web-lab。例如,在请求处理流程的不同部分打印消息,如下所示:

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

为了能够编写更复杂的中间件,实际上需要实现两个特征- Service<Req>这是为了实现实际的中间件本身以及Transform<S, Req>这是处理请求的实际服务的构建器所必需的。

对于实际的中间件实现,通过中间件的构建器Transform特征:

use std::{future::{ready, Ready, Future}, pin::Pin};use actix_web::{dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},web, Error,};pub struct SayHi;impl<S, B> Transform<S, ServiceRequest> for SayHiwhereS: 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 }))}}

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

pub struct SayHiMiddleware<S> {service: S,}type LocalBoxFuture<T> = Pin<Box<dyn Future<Output = T> + 'static>>;impl<S, B> Service<ServiceRequest> for SayHiMiddleware<S>whereS: 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>>;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)})}}

编写好中间件,就可以通过App::new()定义一个app将其添加应用程序中:

#[actix_web::main]async fn main() -> std::io::Result<()> {let app = App::new().wrap(SayHi);// 其他代码}静态文件

Web应用的静态文件出来也必不可收,在Actix Web中简单、简洁的静态文件服务是通过actix_filescrate。可以通过Cargo add添加它,如下所示:

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。

请注意,在任何情况下都不要尝试使用.*路径尾部返回一个文件,这会产生严重的安全隐患,并且会打开Web服务以进行路径遍历,这会引起路径遍历类的攻击。为了防止这种情况,可以尝试验证路径文件是否正确,或者是否尝试使用std::fs::canoncalize遍历目标文件夹之外。

然而,当需要提供多个文件时。例如,需要提供HTML文件的文件夹。为了能够从网络服务提供文件文件夹,最好的方法是使用actix_files::Files:

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}

请注意,还可以使用Filesstruct的多个选项来增强其功能,比如在基本路径上显示文件目录本身(用于文件服务)并允许隐藏文件,更多信息可以参考该板条箱的官方文档。

页面模板

此外,还可以使用HTML模板的强大功能askama增强HTML文件服务:

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

该命令增加了askama本身以及Responder的特征实现askama::Template类型。 askama期望文件位于项目根目录的子文件夹中,名为templates默认情况下,所以先要创建该文件夹,然后创建一个index.html模板文件,其内容(支持HTML格式)最简单的:

Hello, {{name}}!

要在应用程序中使用Askama,则需要声明一个使用Template导出宏并使用assama的template宏将结构指向它使用的模板文件(此处为index.html):

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

然后可以将其添加为Actix Web服务中的常规处理程序函数。当一个访问请求到index_route路径是,服务器就会返回“Hello Chongchong!” 作为响应。更多Web模板和askama板条箱的内容,可参考官方文档。

部署

一般来说,由于必须使用Dockerfile,使用Rust后端程序进行部署可能不太理想,尽管如果已经有使用Docker的经验,则非常简单。当然使用cargo-chef和Shuttle也支持一件部署:

cargo shuttle deploy总结

Rust不光是一个底层的系统级语言,也很适合做Web,数据处理和其他类型的开发。Actix Web是一个强大的框架,可以用它来增强Rust产品组合,如果想构建Rust API或者其他需要Web化的创意,使用Rust,特别是Actix Web也能给以一种“一飞冲天”的体验。

1 阅读:325
评论列表
  • 2024-01-13 16:03

    rust确实好,但是开发真的慢,难度很高,且市场上很难找到工作,中小企业根本用不上,大厂坑位就那么多,且对于新手自学入坑,没有项目持续性加持巩固,学了用不上,过了一两个月又会忘的差不多,若是有企业内部转岗那倒是挺好,且rust目前基本用于重写系统底层,web应用还是需要再发展一段时间

  • 2024-03-03 10:32

    我是外行人,自学编程,还是更爱python,太容易上手了[得瑟][得瑟][得瑟][得瑟]

安全运维得看我

简介:感谢大家的关注