探索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的语法和开发技巧。在学习的过程中克服挑战、深入理解语言本质以及将其应用于实际项目中将给你带来更大的成就感。这是在软件开发领域中锻炼自己技能的绝佳机会。

相关推荐
凌冰_10 分钟前
IDEA2023 SpringBoot整合MyBatis(三)
spring boot·后端·mybatis
码农飞飞18 分钟前
深入理解Rust的模式匹配
开发语言·后端·rust·模式匹配·解构·结构体和枚举
一个小坑货20 分钟前
Rust 的简介
开发语言·后端·rust
qq_1728055939 分钟前
RUST学习教程-安装教程
开发语言·学习·rust·安装
monkey_meng1 小时前
【遵守孤儿规则的External trait pattern】
开发语言·后端·rust
Estar.Lee1 小时前
时间操作[计算时间差]免费API接口教程
android·网络·后端·网络协议·tcp/ip
新知图书2 小时前
Rust编程与项目实战-模块std::thread(之一)
开发语言·后端·rust
盛夏绽放2 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
Ares-Wang2 小时前
Asp.net Core Hosted Service(托管服务) Timer (定时任务)
后端·asp.net
uzong3 小时前
7 年 Java 后端,面试过程踩过的坑,我就不藏着了
java·后端·面试