0
点赞
收藏
分享

微信扫一扫

Actix-Web构建一个简单的HTTP服务器

mjjackey 2022-04-08 阅读 75

Actix-Web框架是目前性能上数一数二的rust的web框架,它的性能远超spring,nestJS, gin等目前流行的框架。
性能排名
仅仅是返回hello,wold,我发现actix-web的性能就已经是gin的3倍。但是使用actix-web的缺点也非常明显,就是代码通过编译非常困难(也许是我太菜了)。

我实践了actix-web的以下功能:

  • 静态文件
  • 错误处理
  • post一个json参数

本服务器用于提交易班账号信息

首先在文件结构上
文件结构
page页面用于存放静态页面,src内容是代码文件,configure_parse用于解析配置文件,配置文件格式是toml,db_handler用于处理数据库,response用于统一处理错误。APITEST用于测试接口。main.rs是主函数。开发过程中使用命令cargo watch -x run,可以热编译。需要安装:cargo install cargo-watch

大概需要下面这些依赖

[dependencies]
actix-web = "4"
actix-files = "0.6.0"
derive_more = "0.99.17"
sqlx = { version = "0.5.11", features = ["runtime-actix-native-tls", "mysql"] }
exitcode = "1.1.2"
serde = { version = "1.0.136" ,features=["derive"]}
serde_json = "1.0.79"
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
validator = { version = "0.12", features = ["derive"] }
toml = "0.5.8"

HTTP服务器

官方的API文档给出了一个简单的例子

use actix_web::{get, web, App, HttpServer, Responder};

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

#[actix_web::main] // or #[tokio::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new()
            .route("/hello", web::get().to(|| async { "Hello World!" }))
            .service(greet)
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

在宏的加持下,使用rust构建HTTP服务器也是比较简单的,只需要简简单单的几行代码就可以搞定。

静态文件

显示静态文件也不是非常困难,我们在网页路径和本地的路径之间建立映射关系。
文件映射
为了在网站的根目录也能显示网页,我们还需要单独处理一下get(“/”)这种情况,我们稍稍修改一下hello函数

use actix_files::{NamedFile};
#[get("/")]
async fn hello() -> Result<NamedFile> {
    Ok(NamedFile::open(PathBuf::from("./page/index.html"))?)
}

这样就可以显示静态页面了
静态网页

统一处理错误


// response/result.rs
use actix_web::{error, HttpResponse, HttpResponseBuilder};
use actix_web::body::BoxBody;
use actix_web::http::{header, StatusCode};
use derive_more::{Display, Error};
use serde_json::json;


#[derive(Display, Debug)]
pub enum YiBanResponseError {
    #[display(fmt = "格式化错误 :{}", _0)]
    FormatError(&'static str),
    #[display(fmt = "服务器错误 : {}", _0)]
    ServerError(&'static str),
    #[display(fmt = "数据库错误 : {}", _0)]
    DBError(&'static str),
    #[display(fmt = "未找到目标 : {}", _0)]
    NotFoundError(&'static str),
}


impl error::ResponseError for YiBanResponseError {
    fn error_response(&self) -> HttpResponse {
        HttpResponseBuilder::new(self.status_code())
            .insert_header(("Content-Type", "application/json; charset=utf-8"))
            .json(json!({
                "success":false,
                "message":self.to_string()
            }))
    }

    fn status_code(&self) -> StatusCode {
        match *self {
            YiBanResponseError::FormatError(_) => StatusCode::from_u16(510).unwrap(),
            YiBanResponseError::ServerError(_) => StatusCode::from_u16(511).unwrap(),
            YiBanResponseError::DBError(_) => StatusCode::from_u16(512).unwrap(),
            YiBanResponseError::NotFoundError(_) => StatusCode::from_u16(513).unwrap()
        }
    }
}

我们在需要返回错误的地方使用.map_err方法将错误映射为YibanResponseError,这将会相当方便,例如
数据库错误

#[get("/error")]
async fn return_error() -> Result<&'static str, YiBanResponseError> {
    Err(YiBanResponseError::ServerError("调试错误"))
}

连接MySQL数据库

这里需要使用sqlx,用法可以去看github的项目地址

连接

    let pool = MySqlPoolOptions::new()
        .max_connections(50)
        .connect(&format!("mysql://{}:{}@{}:{}/yiban",
                          "", "","","",""))
        .await.unwrap_or_else(|_| { std::process::exit(exitcode::OK) });

放在appdata中

这是一个小技巧,我们把pool放在APPDATA中,放在这里面的数据是共享的,可以供每个router的handler函数调用,我们将在下面看到这会非常方便。

HttpServer::new(move || {
        App::new()
            .app_data(web::Data::new(AppState {
                app_name: String::from("Actix-web"),
                pool: pool.clone(),
            }))
            .service(hello)
            .service(fs::Files::new("/", "./page").show_files_listing())
            .service(fs::Files::new("/js", "./page/js").show_files_listing())
            .service(fs::Files::new("/css", "./page/css").show_files_listing())
            .service(fs::Files::new("/img", "./page/img").show_files_listing())
    })
        .bind(("127.0.0.1", 8080))?
        .run()
        .await

测试驱动开发

我们在函数上标注宏#[actix_web::test],就可以对这个函数进行测试。
测试函数

向数据库插入数据

这里面我们主要使用了serde,sqlx,里面的一些宏使得我们可以给函数实现默认接口,这会非常方便。

// ./db_hander/user.rs
use actix_web::ResponseError;
use sqlx;
use serde::{Serialize, Deserialize};
use serde::de::Unexpected::Str;
use sqlx::mysql::MySqlPoolOptions;
use sqlx::{FromRow, MySql, Pool};


#[derive(Serialize, Deserialize, Debug, FromRow)]
pub struct User {
    name: String,
    #[serde(rename(serialize = "phone_number", deserialize = "phone"))]
    #[sqlx(rename = "phone_number")]
    phone: String,
    data: String,
    password: String,
    // #[serde(default = "")]
    #[serde(skip_deserializing)]
    csrf: String,
    // #[serde(default = "")]
    #[serde(skip_deserializing)]
    phpsessid: String,
}

impl User {
    pub fn new(name: &str, password: &str, phone: &str, data: &str) -> User {
        return User {
            name: String::from(name),
            password: String::from(password),
            csrf: "".to_string(),
            phone: String::from(phone),
            data: String::from(data),
            phpsessid: "".to_string(),
        };
    }
    pub async fn insert_data(self, pool: &Pool<MySql>) -> Result<(), sqlx::Error> {
        sqlx::query("insert into yiban.user(name,password,phone_number,data) values (?,?,?,?)")
            .bind(self.name)
            .bind(self.password)
            .bind(self.phone)
            .bind(self.data)
            .execute(pool)
            .await?;
        Ok(())
    }
}



#[actix_web::test]
async fn insert_data_test() -> sqlx::Result<()> {
    println!("向数据库中插入数据的测试开始");
    let pool = MySqlPoolOptions::new()
        .max_connections(50)
        .connect("mysql://root:root@host:2222/user")
        .await?;

 
    User::new("哈哈哈", "12345", "110", r#"{"aa":"bb"}"#)
        .insert_data(&pool).await?;
    Ok(())
}

处理post请求

这里我们直接从data中拿到了pool进行数据库的操作

#[post("/fuck")]
async fn collect_info(mut payload: web::Payload, data: web::Data<AppState>) -> Result<HttpResponse, Error> {
    // payload is a stream of Bytes objects
    let mut body = web::BytesMut::new();
    while let Some(chunk) = payload.next().await {
        let chunk = chunk?;
        // limit max size of in-memory payload
        if (body.len() + chunk.len()) > MAX_SIZE {
            return Err(error::ErrorBadRequest("overflow"));
        }
        body.extend_from_slice(&chunk);
    }

    // body is loaded, now we can deserialize serde-json
    let user = serde_json::from_slice::<User>(&body)?;
    let pool = &data.pool;
    user.insert_data(pool).await.map_err(|_| YiBanResponseError::DBError("数据已经存在或者格式不正确"))?;
    Ok(HttpResponse::Ok().json(json!({
        "success":true,
        "message":"提交成功"
    }))) // <- send response
}

配置文件

这里我们使用toml文件配置一些参数

// config_parser/parser.rs


use std::{fs, path};
use std::io::{Error, Read};
use actix_web::dev::Path;
use actix_web::web::to;
use serde::{Serialize, Deserialize};
use serde::de::Unexpected::Str;
use crate::YiBanResponseError;

#[derive(Serialize, Deserialize, Debug)]
pub struct Config {
    pub(crate) dbconfig: DBConfig,
    appconfig: APPConfig,
}


#[derive(Serialize, Deserialize, Debug)]
pub struct DBConfig {
    pub(crate) username: String,
    pub(crate) password: String,
    pub(crate) host: String,
    pub(crate) port: u16,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct APPConfig {
    app_name: String,
}

pub async fn config(path: &str) -> Result<Config, actix_web::Error> {
    let mut file = fs::File::open(path).unwrap();
    let mut config_str = String::new();
    file.read_to_string(&mut config_str);
    let config: Config = toml::from_str(&config_str)
        .map_err(|_| YiBanResponseError::ServerError("配置文件解析错误"))?;
    println!("{:#?}", config);
    Ok(config)
}

#[actix_web::test]
async fn parse_config_test() {
    let mut file = fs::File::open("./Config.toml").unwrap();
    let mut config_str = String::new();
    file.read_to_string(&mut config_str);
    let config: Config = config("./Config.toml").await.unwrap();
    println!("{:#?}", config)
}

性能

以GET /js/chunk-vendors.cc18ca34.js测试性能

请求4000,并发数4000

actix-web性能:
actix速率测试
gin性能:
gin性能

请求4000,并发数1

actix-web
gin
gin

举报

相关推荐

0 条评论