探索Actix Web:高性能异步 Rust Web框架

Actix Web 是一款基于 Rust 语言开发的高性能 Web 框架。它通过异步编程模型、强大的请求路由、中间件支持,为开发者提供了丰富的工具和选项,是构建可伸缩、高并发的 Web 应用程序的理想选择。

初始化

新建工程actix-test。

bash 复制代码
cargo new actix-test
cd actix-test

Cargo.toml增加依赖:

toml 复制代码
[dependencies]
actix-web = "4"

接口代码

修改src/main.ts:

rust 复制代码
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()
        .cookie(
            Cookie::build("name", "value")
                .domain("localhost")
                .path("/")
                .secure(false)
                .http_only(true)
                .finish(),
        )
        .body("Hey there!")
}

#[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,运行服务。

http://127.0.0.1:8080

http://127.0.0.1:8080/hey

还有个post请求:

javascript 复制代码
fetch('echo', {method: 'post', body: JSON.stringify({a: 'b'}), headers: {'content-type':'application/json'}});

参数

参数获取

path

url路径中的参数中可以使用web::Path来整合成一个Struct对象:

rust 复制代码
use actix_web::{get, web, App, HttpServer, 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
    ))
}

这里用到了serde::Deserialize,所以需要在toml里添加依赖:

toml 复制代码
serde =  {version = "1", features =["derive"]}

要使用derive,就必须这样添加features,否则会报

systemverilog 复制代码
cannot find derive macro Deserializ in this scope

也可以用web::Path<T>只取某一个参数:

rust 复制代码
#[get("/hello/{name}")]
async fn greet(name: web::Path<String>) -> impl Responder {
    format!("Hello {name}!")
}

Query

Query与path类似,可以获取/hello?name=test&age=16中的name与age。

rust 复制代码
#[derive(serde::Deserialize)]
struct MyParams {
    name: String,
    age: u16,
}

#[get("/hello")]
async fn hello(params: web::Query<MyParams>) -> String {
    format!("Hello, {}! You are {} years old.", params.name, params.age)
}

POST

对于POST请求,可以使用web::Json获取Body中的参数:

rust 复制代码
#[derive(serde::Deserialize)]
struct MyParams {
    name: String,
    age: u8,
}

#[post("/hello")]
async fn hello(params: web::Json<MyParams>) -> impl Responder {
    HttpResponse::Ok().body(format!(
        "Hello, {}! You are {} years old.",
        params.name, params.age
    ))
}

参数校验

参数校验通常发生在POST请求里,因为它的参数通常比较复杂。如果每个接口都写一遍参数校验处理,那就太Low了。可以使用validator来简化代码。

rust 复制代码
use serde::{Deserialize, Serialize};
use validator::Validate;

#[derive(Deserialize, Debug, Serialize, Validate)]
pub struct CreateJobDto {
    #[validate(length(min = 10, max = 100))]
    pub old_image: String,

    #[validate(length(min = 10, max = 100))]
    pub new_image: String,

    pub status: JobStatus,
    pub user_id: String,
}


use actix_web::error::ErrorBadRequest;
use actix_web::Error;

#[post("createJob")]
pub async fn create_job(
    db: web::Data<Database>,
    params: web::Json<CreateJobDto>,
) -> Result<HttpResponse, Error> {
    params.validate().map_err(ErrorBadRequest)?;
    match job_service::create_job(&db, params.into_inner()).await {
        Ok(id) => Ok(HttpResponse::Ok().body(id)),
        Err(err) => {
            if err.to_string().contains("E11000 duplicate key error") {
                Ok(HttpResponse::BadRequest().body("old_image重复"))
            } else {
                log::error!("error: {}", err);
                Ok(HttpResponse::InternalServerError().body(err.to_string()))
            }
        }
    }
}

Guard

rust 复制代码
config.service(
        web::scope("/api/user")
            .guard(guard::Header("x-guarded", "secret"))
            .service(auth_controller::login));

这里的Guard与NestJS中意义上不同的是,它只是保护接口,被过滤掉的接口只会响应404。

所以要做到NestJS中的接口保护,只能写中间件。下面来具体介绍如何实现。

简单Guard

这是一个不需要注入用户信息的普通Guard:

rust 复制代码
use crate::globals;
use actix_web::{
    dev::{self, Service},
    error::ErrorForbidden,
};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse, Transform},
    Error,
};
use futures_util::future::LocalBoxFuture;
use std::{
    future::{ready, Ready},
    rc::Rc,
};

pub struct SimpleGuard;

impl<S: 'static, B> Transform<S, ServiceRequest> for SimpleGuard
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 = SimpleGuardMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(SimpleGuardMiddleware {
            service: Rc::new(service),
        }))
    }
}
pub struct SimpleGuardMiddleware<S> {
    // This is special: We need this to avoid lifetime issues.
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for SimpleGuardMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    dev::forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let svc = self.service.clone();

        Box::pin(async move {
            let is_valid = match req.headers().get("Authorization") {
                Some(token) => {
                    let tk = token.to_str().unwrap_or_default();
                    if tk == ("Bearer ".to_owned() + &globals::CONFIG.token) {
                        true
                    } else {
                        warn!("Authorization [{}] is invalid", tk);
                        false
                    }
                }
                None => false,
            };
            if !is_valid {
                return Err(ErrorForbidden("invalid token"));
            }

            let res = svc.call(req).await?;
            // println!("response: {:?}", res.headers());
            Ok(res)
        })
    }
}

SSOGuard

中间件怎么往下一步流转添加信息呢?

查看actix-web官方的样例,找到这样一段代码:

rust 复制代码
 #[derive(Clone)]
pub struct User {
    name: String,
}

impl User {
    fn new(name: String) -> Self {
        User { name: name }
    }
}

// 在中间件中注入额外信息
request.extensions_mut().insert(User::new("test".to_string()));

在接口中使用:

rust 复制代码
#[get("/login")]
pub async fn login(req: HttpRequest, user: ReqData<User>) -> impl Responder {
    info!("username is {:?}", user.name);
    "login"
}

#[get("/login")]
pub async fn login(req: HttpRequest, user: Option<ReqData<User>>) -> impl Responder {
    if let Some(user) = user {
        info!("username is {:?}", user.name);
    }
    "login"
}

如果不加Option类型,则如果没有找到这个信息,就会报错。

以下是一份SSO Guard的代码:

rust 复制代码
use super::user_service::get_user_info;
use actix_web::{
    dev::{self, Service},
    error::ErrorForbidden,
    error::ErrorUnauthorized,
    HttpMessage,
};
use actix_web::{
    dev::{ServiceRequest, ServiceResponse, Transform},
    Error,
};
use futures_util::future::LocalBoxFuture;
use std::{
    future::{ready, Ready},
    rc::Rc,
};

pub struct SSOGuard;

impl<S: 'static, B> Transform<S, ServiceRequest> for SSOGuard
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 = SSOGuardMiddleware<S>;
    type Future = Ready<Result<Self::Transform, Self::InitError>>;

    fn new_transform(&self, service: S) -> Self::Future {
        ready(Ok(SSOGuardMiddleware {
            service: Rc::new(service),
        }))
    }
}
pub struct SSOGuardMiddleware<S> {
    // This is special: We need this to avoid lifetime issues.
    service: Rc<S>,
}

impl<S, B> Service<ServiceRequest> for SSOGuardMiddleware<S>
where
    S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error> + 'static,
    S::Future: 'static,
    B: 'static,
{
    type Response = ServiceResponse<B>;
    type Error = Error;
    type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;

    dev::forward_ready!(service);

    fn call(&self, req: ServiceRequest) -> Self::Future {
        let svc = self.service.clone();

        Box::pin(async move {
            let result = get_user_info(req.headers().to_owned()).await;
            let user_info = match result {
                Ok(userinfo) => userinfo,
                Err(err) => {
                    warn!("fetch user info failed, {}", err);
                    return Err(ErrorUnauthorized("unauthorized"));
                }
            };
            if !user_info.internal {
                warn!("outernal user try to access: {user_info:?}");
                return Err(ErrorForbidden("forbidden"));
            } else {
                info!(
                    "校验通过,得到用户信息,id: {}, name: {}",
                    &user_info.openid,
                    &user_info.get_name()
                );
            }
            req.extensions_mut().insert(user_info);
            let res = svc.call(req).await?;
            // println!("response: {:?}", res.headers());
            Ok(res)
        })
    }
}

在接口中使用:

rust 复制代码
#[get("userInfo")]
pub async fn get_user_info(user: ReqData<SSOUserInfo>) -> impl Responder {
    HttpResponse::Ok().json(user.into_inner())
}

总结

Rust是一种现代、高效的编程语言,具有静态类型系统、内存安全和并发处理等功能。学习Rust不仅可以提高你的编程技能,还可以让你编写更具可靠性和性能的软件。本文简单介绍了Actix-web这款功能强大、性能出色且易于使用的 Rust Web 框架,给出一些参数获取与校验的样例,以及如何使用中间件实现NestJS中的接口守卫功能。希望以上内容能帮助你快速掌握这款框架。

值得一提的是,随着ChatGPT的出现,我认为极大地降低了Rust的学习成本,尤其是语法层面,ChatGPT完全可以帮助你解决大部分日常使用的问题。所以,对Rust感兴趣,或者想要入手一门系统级开发语言的话,勇敢地行动吧,现在正是最好的时机。

当然,ChatGPT可以帮助你解决一些问题,但仍然需要在实践中掌握Rust的语法和开发技巧。在学习的过程中克服挑战、深入理解语言本质以及将其应用于实际项目中将给你带来更大的成就感。这是在软件开发领域中锻炼自己技能的绝佳机会。

相关推荐
爱勇宝43 分钟前
第 1 章:别把“需求文档”当成真正的需求
前端·后端·程序员
花褪残红青杏小2 小时前
Rust图像处理第12节-图片翻转与旋转:坐标重映射几何变换
rust·webassembly·图形学
IT_陈寒5 小时前
闭包陷阱让我加了两天班,JavaScript你真行
前端·人工智能·后端
易协同低代码6 小时前
通达OA核心类库TD类深度解析
后端
Gopher_HBo6 小时前
Go语言学习笔记(十八)Gin处理Session
后端
谭光志7 小时前
工具塞满上下文窗口怎么办?深度拆解 AI Agent Tool Search 按需加载实现原理
前端·后端·ai编程
她说..7 小时前
Java 默认值设置方式
java·开发语言·后端·springboot
foggyprojects7 小时前
从0开始,一句话启动AI DataAgent
后端·数据分析·ai编程
郡杰7 小时前
一些基础和问题解决
后端
陈随易8 小时前
前端项目部署只要30秒
前端·后端·程序员