Actix Web学习总结

831 阅读5分钟

安装Rust环境

Windows

  1. Microsoft Visual Studio C++ 生成工具 ,需要下载Visual Studio 2022生成工具。进行安装时选择C++生产工具和Windows 10 sdk

  2. 通过下载安装包安装,或者在PowerShell使用命令安装

    winget  --id Rustlang.Rustup
    

注意安装后需要重启命令窗口,并且生成工具需要安装完成,不然识别不到rustc命令

macOS

  1. CLang和macOS开发依赖项

    $ xcode-select --install

  2. Rust

    $ curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

Linux

  • 版本较多具体看文末参考链接

创建项目

  1. 使用命令创建一个基于二进制的Cargo项目,并进入项目目录
 cargo new hello-world
 cd hello-world
  1. 将依赖的crate加入Cargo.toml
[dependencies]
actix-web = "4"
  1. 创建请求处理函数
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().body("Hey there!")
}

值得注意的是:这里manual_hello函数没有设置路由

  1. 创建服务
#[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
}

这样一个简单的web服务器就完成了。服务绑定在本地的8080端口,现在我们有了三个接口。

拆分模块

rust提供了强大的模块系统,可以将代码拆分成多个模块。使得模块的功能分工明确,可以更好的组织、重用代码。 actix在Application实例上提供了一个configure方法

  1. 首先在src文件夹下创建一个user.rs 文件
  2. user.rs 模块需要向外暴露一个config方法
pub fn config(cfg:&mut web::ServiceConfig){
    cfg.service(
        web::scope("user") //这里代表路由地址以user开头,例如/user/info。其次这里写”user“或者”/user“都可以actix会自动补上”/“
        .service(index)
        .service(vaildtest)
    );
}

可以看到该方法接受一个可变借用cfg,在cfg上面注册路由服务与和main中完全相同。

3.回到main.rs

mod user; //引入创建的user模块

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .service(hello)
            .configure(user::config)  //添加到application上面
            .service(echo)
            .route("/hey", web::get().to(manual_hello))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

请求处理

  1. 获取请求路径中的参数,例如/user/123/前端,需要获取某个id为123,工作为前端的用户的信息
#[get("/{user_id}/{job}")]
async fn get_user_info(info:web::Path<(u32,String)>)->impl Responder{
    let (user_id,job)=info.into_inner();
    format!("user_id:{},job:{}",user_id,job)
}

不要忘记注册这个路由服务,其次可以注意到这里返回了一个字符串,同时函数前面写的是返回实现了Responder这个trait,actix_web提供了一些类型的默认实现更多可以查阅

  1. 处理json数据的post请求
  • 添加依赖
[dependencies]
serde_json = "0.6.2"
futures = "0.3"
serde = { version = "1.0", features = ["derive"] }
  • 代码实现
use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Deserialize, Serialize)]
struct Info {
    user_id: i32,
    job: String,
}

#[post("/submit")]
async fn submit(info: web::Json<Info>) -> Info{
    Info{
        user_id:info.user_id,
        job:info.job.clone()
    }
}

代码很简单创建了一个Info结构体,给它加了两个trait,然后通过web::json解析出了数据。值得注意的是这里我直接返回了Info。没错,因为我这里为Info实现了Responder这个trait

impl Responder for Info {
    type Body = BoxBody;
    fn respond_to(self, req: &actix_web::HttpRequest) -> HttpResponse<Self::Body> {
        let body = serde_json::to_string(&self).unwrap();
        HttpResponse::Ok()
            .content_type(ContentType::json())
            .body(body)
    }
}
  1. 处理formdata的post请求,前端经常会回传表单数据,里面除了一些信息外还可能有文件
  • 老规矩先添加依赖
[dependencies]
actix-multipart = "0.6.0"
actix-files = "0.6.0"
  • 定义结构体
use actix_multipart::{
    form::{
        tempfile::{TempFile, TempFileConfig},
        text::Text,
        MultipartForm,
    },
    Multipart,
};
use serde_json::Value;
#[derive(Debug,MultipartForm)]
struct UploadForm{
#[multipart(rename="file")]
files:Vec<TempFile>,
name:Text<String>,
id:Text<i32>
crates:Text<String>
}
  • 请求处理函数
#[post("/form")]
async fn save_files(MultipartForm(form): MultipartForm<UploadForm>) -> Result<impl Responder> {
    for f in form.files {
        let path = format!("./tmp/{}", f.file_name.unwrap());
        f.file.persist(path).unwrap();
    }
    let res = format!("id:{},name:{}", form.id.0, form.name.0);
    let crates = form.crates.0.as_str();
    let crates: Value = serde_json::from_str(crates)?; //将合法json字符串转json
    println!("crates:{:#?}", crates);
    Ok(HttpResponse::Ok().body(res))
}

自定义错误

  • 定义结构体
use derive_more::{Display,Error}
#[derive(Debug,Display,Error)]
enum MyError {
    #[display(fmt="internal error")]
    InternalError,
    #[display(fmt = "bad request")]
    BadClientData,
    #[display(fmt = "timeout")]
    Timeout,
    #[display(fmt="vaildation error on filed:{}",filed)]
    ValidationError{filed:String}
}

这里使用了derive_more来简化Display这个trait的实现,因为我们后面需要用到to_string方法

  • 自定义错误需要实现actix_web::error::ResponseError这个trait,它上面有两个方法需要实现具体代码如下
use actix_web::{get,web, error, HttpResponse, http::{header::ContentType, StatusCode}};
impl  error::ResponseError for MyError {
    fn error_response(&self) -> actix_web::HttpResponse<actix_web::body::BoxBody> {
        HttpResponse::build(self.status_code())
        .insert_header(ContentType::html())
        .body(self.to_string())
    }
    fn status_code(&self) -> actix_web::http::StatusCode {
        match  *self {
            MyError::InternalError=>StatusCode::INTERNAL_SERVER_ERROR,
            MyError::BadClientData=>StatusCode::BAD_REQUEST,
            MyError::Timeout=>StatusCode::GATEWAY_TIMEOUT,
            MyError::ValidationError { ..}=>StatusCode::BAD_REQUEST
        }
    }
}

status_code返回请求的错误码,error_response对请求进行返回

  • 自定义错误的使用
#[get("/test")]
async fn index()->Result<&'static str,MyError>{
    Err(MyError::BadClientData)
}

#[get("/vaildtest")]
async fn vaildtest() ->Result<&'static str,MyError>{
    Err(MyError::ValidationError { filed: "input is invalid".to_string() })
}

日志(中间件)

Actix Web的中间件系统允许我们为请求/响应处理添加额外的行为。通过日志中间件可以记录下接口请求的日志,

  • 使用crate
[dependencies]
env_logger = "0.8" //设置环境变量
  • 使用
#[rustfmt::skip]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
    std::env::set_var("RUST_LOG", "error");// 设置日志级别
    std::env::set_var("RUST_BACKTRACE", "1"); //此处暂时不明白,望懂的人解惑
    env_logger::init();

    HttpServer::new(|| {
        let logger=Logger::default();
        App::new()
            .wrap(logger)
            .configure(basic::config)
            .service(
                web::resource("/user/{name}")
                    .name("user_detail")
                    .guard(guard::Header("content-type", "application/json"))
                    .route(web::get().to(HttpResponse::Ok))
                    .route(web::put().to(HttpResponse::Ok)),
            )
            .service(echo)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

日志级别:error>warn>info>debug>trace ,这个级别有什么用?级别越低日志越详细,举个例子:当设置error时普通的接口请求不会记录日志

静态文件使用

use actix_files::Files;
.service(Files::new("/static","dist/static/").show_files_listing())
.service(Files::new("/","dist/").index_file("index.html"))

这里不放完整的代码了,这边第一个service请求“/static”开头路径时,映射到dist/static文件夹下的所有文件 ,第二个serivce 访问”/“即根路由时返回index.html。注:该路径配置下dist文件夹放在与src同级

构建与运行

  1. 构建
cargo build --release

文件会生成在target/release/hello-word,因为项目名为hello-world 2. 创建一个新的文件夹将dist文件和hello-word拷贝到这个新文件夹,从终端打开该文件夹

$ nohup ./hello-word &

启动后会生成一个nohup.out的日志文件

参考