天天看點

# Rust Web入門(二):Actix

本教程筆記來自 楊旭老師的 rust web 全棧教程,連結如下:

https://www.bilibili.com/video/BV1RP4y1G7KF?p=1&vd_source=8595fbbf160cc11a0cc07cadacf22951

學習 Rust Web 需要學習 rust 的前置知識可以學習楊旭老師的另一門教程

https://www.bilibili.com/video/BV1hp4y1k7SV/?spm_id_from=333.999.0.0&vd_source=8595fbbf160cc11a0cc07cadacf22951

今天來入門基于 rust 的 web 架構 Actix:

Actix簡單使用

Actix - Rust 的 Actor 異步并發架構

Actix 基于 Tokio 和 Future,開箱具有異步非阻塞事件驅動并發能力,其實作低層級 Actor 模型來提供無鎖并發模型,而且同時提供同步 Actor,具有快速、可靠,易可擴充。

Actix 之上是高性能 Actix-web 架構,很容易上手。使用 Actix-web 開發的應用程式将在本機可執行檔案中包含 HTTP 伺服器。你可以把它放在另一個像 nginx 這樣的 HTTP 伺服器上。但即使完全不存在另一個 HTTP 伺服器 (像 nginx) 的情況下,Actix-web 也足以提供 HTTP 1 和 HTTP 2 支援以及 SSL/TLS。這對于建構微服務分發非常有用。

我們需要先建立一個項目,然後引入需要的依賴,然後使用 bin 指定我們的 bin 目錄

[package]
name = "stage_2"
version = "0.1.0"
edition = "2021"

[dependencies]
actix-web = "3"
actix-rt = "1.1.1"

[[bin]]
name = "server1"
           

之後我們在 src 下建立一個 bin 目錄和一個 server1.rs 編寫我們的架構:

對于 server1.rs 我們需要初始化一個 app 作為我們的 web 項目,然後為它配置一個路由的函數,之後再指定的端口運作我們的 app 項目。因為它是異步的,是以我們要加上 await 和 async 進行修飾并且使用 actix_rt::main 這個包

use actix_web::{web, App, HttpResponse, HttpServer, Responder};
use std::io;
#[actix_rt::main]
async fn main() -> io::Result<()> {
    let app = move || App::new().configure(general_routes);
    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}
           

之後我們編寫我們的路由函數,它傳入一個配置項,你可以在其中配置對應路由的處理方法,比如我們處理 /health 路徑的 get 方法,我們就可以用如下的方式進行編寫,在 to 之後提供一個函數作為我們的處理函數。

處理函數是需要實作 Responder 這個 Trait 的,是以我們的傳回值需要使用 HttpResponse 相關的函數進行傳回,其中 Ok() 表示 200 這個狀态碼,之後又使用 json 函數傳回了一段 json 作為作為我們的傳回值

pub fn general_routes(cfg: &mut web::ServiceConfig) {
    cfg.route("/health", web::get().to(health_check_handler));
}

pub async fn health_check_handler() -> impl Responder {
    HttpResponse::Ok().json("Actix Web Service is running!")
}
           

現在我們的建立搭建完畢了,我們在指令行啟動我們的項目,然後通路 120.0.0.1:3000 ,可以看到,Actix Web Service is running! 這句話,那麼我們的項目就可以正常使用了

建構完整的 rust API

現在我們已經可以運作我們的 Actix 架構了,之後我們來嘗試建構一個完整的具有增删改查功能的 api,我們再建立一個 teacher-service.rs 把這個項目設定為預設項目,并且加載我們需要的包:

[package]
name = "stage_3"
version = "0.1.0"
edition = "2021"
default-run = "teacher-service"

[dependencies]
actix-web = "3"
actix-rt = "1.1.1"
serde = { version = "1.0.132", features = ["derive"] }
chrono = { version = "0.4.19", features = ["serde"] }

[[bin]]
name = "server1"

[[bin]]
name = "teacher-service"
           

資料庫的部分将會在下一部分講解,我們先把我們的資料放在記憶體中,我們先建立一個 models.rs 它用于定義我們的資料結構, 通過剛剛引入的 serde 包,我們可以讓 json 資料轉化為我們的資料結構

use actix_web::web;
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};

#[derive(Deserialize, Serialize, Debug, Clone)]
pub struct Course {
    pub teacher_id: usize,
    pub id: Option<usize>,
    pub name: String,
    pub time: Option<NaiveDateTime>,
}

impl From<web::Json<Course>> for Course {
    fn from(course: web::Json<Course>) -> Self {
        Course {
            teacher_id: course.teacher_id,
            id: course.id,
            name: course.name.clone(),
            time: course.time,
        }
    }
}
           

之後我們編寫一個 state.rs 封裝我們全局共享的資料結構,它包括一個響應,一個通路次數和一個傳回的結構體,這個内容将作為全局内容在我們的程式中共享,因為涉及到多個程式會調用 visit_count 和 courses 資料,是以我們把他們放在 Mutex 中來保證互斥調用:

use std::sync::Mutex;

use crate::modelds::Course;

pub struct AppState {
    pub health_check_response: String,
    pub visit_count: Mutex<u32>,
    pub courses: Mutex<Vec<Course>>,
}
           

之後将上一步簡單 get 方法的路由配置到這裡,我們建立 routers.rs 來存放路由

use super::handlers::*;
use actix_web::web;
pub fn general_routes(cfg: &mut web::ServiceConfig) {
    cfg.route("/health", web::get().to(health_check_handler));
}
           

然後建立一個 handlers.rs 方法來定于我們的對于路由的處理函數,這裡我們可以調用全局注冊的 app_state ,這個内容會在下一部分講到。我們取出共享資料裡的 通路次數和響應内容,之後傳回一個 json 資料。

use super::state::AppState;
use actix_web::{web, HttpResponse};

pub async fn health_check_handler(app_state: web::Data<AppState>) -> HttpResponse {
    println!("incoming for health check");
    let health_check_response = &app_state.health_check_response;
    let mut visit_count = app_state.visit_count.lock().unwrap();
    let response = format!("{} {} times", health_check_response, visit_count);
    *visit_count += 1;
    HttpResponse::Ok().json(&response)
}
           

最後我們配置我們的主函數 teacher-service.rs ,在 3000 端口啟動我們的項目,我們将一個初始化的 shared_data 配置到項目中,之後在項目的整個的流程中都可以使用它

use actix_web::{web, App, HttpServer};
use std::io;
use std::sync::Mutex;

#[path = "../handlers.rs"]
mod handlers;
#[path = "../models.rs"]
mod modelds;
#[path = "../routers.rs"]
mod routers;
#[path = "../state.rs"]
mod state;

use routers::*;
use state::AppState;

#[actix_rt::main]
async fn main() -> io::Result<()> {
    let shared_data = web::Data::new(AppState {
        health_check_response: "I'm OK.".to_string(),
        visit_count: Mutex::new(0),
        courses: Mutex::new(vec![]),
    });
    let app = move || {
        App::new()
            .app_data(shared_data.clone())
            .configure(general_routes)
    };

    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}
           

這樣我們就可以在 127.0.0.1:3000 啟動我們的項目,當你調用 127.0.0.1:3000/health 的時候,你可以看到輸出了

I'm OK. 1 times

,每調用一次,times + 1

處理POST 請求

我們現在已經可以處理 get 請求并且傳回一組預定的資料了,現在我們來嘗試調用 POST 請求來新增我們的資料:

我們首先注冊一個新的路由,它在一個

/courses

的空間中,表示它的所有 api 都必須使用 localhost:3000/courses 開頭,我們先添加一個 localhost:3000/courses 的路由,它是 post 方法,用于新增一條資料

pub fn course_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/courses")
            .route("/", web::post().to(new_course))
    );
}
           

之後我們在 handlers.rs 編寫它的處理函數:我們要做的是把我們收到的資料寫入到 app_state 中,我們先計算出有多少個資料來計算出新增資料的 id 号作為唯一辨別,然後将傳入資料存入我們的全局資料中

要注意,我們需要先擷取所有權,然後将資料克隆一份來計算長度,否則資料在使用完畢以後就被回收了:

use super::modelds::Course;
use chrono::Utc;
pub async fn new_course(
    new_course: web::Json<Course>,
    app_state: web::Data<AppState>,
) -> HttpResponse {
    println!("Received new course");
    let course_count = app_state
        .courses
        .lock()
        .unwrap()
        .clone()
        .into_iter()
        .filter(|course| course.teacher_id == new_course.teacher_id)
        .collect::<Vec<Course>>()
        .len();
    let new_course = Course {
        teacher_id: new_course.teacher_id,
        id: Some(course_count + 1),
        name: new_course.name.clone(),
        time: Some(Utc::now().naive_utc()),
    };
    app_state.courses.lock().unwrap().push(new_course);
    HttpResponse::Ok().json("Course added")
}
           

我們編寫一個測試來測試我們的接口:

mod tests {
    use super::*;
    use actix_web::http::StatusCode;
    use std::sync::Mutex;

    #[actix_rt::test]
    async fn post_course_test() {
        let course = web::Json(Course {
            teacher_id: 1,
            name: "Test course".into(),
            id: None,
            time: None,
        });

        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            courses: Mutex::new(vec![]),
        });
        let resp = new_course(course, app_state).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }
}
           

動态路由

有時候我們希望我們的路徑中帶有我們需要的查詢資料,例如,我們希望通過

/course/1

來查詢對應 id 為1 的老師的課程,通過

/course/1/12

來查詢對應 id 為1 的老師 id 為 12的課程,那麼我們需要建構一個動态路由:

首先我們這樣編寫一個路由,其中的 user_id 和 course_id 可以作為參數提取到,而我們的路徑可以比對到這些路由

pub fn course_routes(cfg: &mut web::ServiceConfig) {
    cfg.service(
        web::scope("/courses")
            .route("/", web::post().to(new_course))
            .route("/{user_id}", web::get().to(get_courses_for_teacher))
            .route("/{user_id}/{course_id}", web::get().to(get_course_detail)),
    );
}

           

之後我們在 handlers 裡編寫處理方法,通過傳入參數 params 可以拿到我們的路徑,我們需要建構我們的查詢來傳回對應的值:

pub async fn get_courses_for_teacher(
    app_state: web::Data<AppState>,
    params: web::Path<usize>,
) -> HttpResponse {
    let teacher_id: usize = params.0;

    let filtered_courses = app_state
        .courses
        .lock()
        .unwrap()
        .clone()
        .into_iter()
        .filter(|course| course.teacher_id == teacher_id)
        .collect::<Vec<Course>>();

    if filtered_courses.len() > 0 {
        HttpResponse::Ok().json(filtered_courses)
    } else {
        HttpResponse::Ok().json("No courses found for teacher".to_string())
    }
}

pub async fn get_course_detail(
    app_state: web::Data<AppState>,
    params: web::Path<(usize, usize)>,
) -> HttpResponse {
    let (teacher_id, course_id) = params.0;
    let selected_course = app_state
        .courses
        .lock()
        .unwrap()
        .clone()
        .into_iter()
        .find(|x| x.teacher_id == teacher_id && x.id == Some(course_id))
        .ok_or("Course not found");
    if let Ok(course) = selected_course {
        HttpResponse::Ok().json(course)
    } else {
        HttpResponse::Ok().json("Course not found".to_string())
    }
}
           

我們也可以為我們編寫的這兩個方法添加測試:

#[actix_rt::test]
    async fn get_all_courses_success() {
        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            courses: Mutex::new(vec![]),
        });
        let teacher_id: web::Path<usize> = web::Path::from(1);
        let resp = get_courses_for_teacher(app_state, teacher_id).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }

    #[actix_rt::test]
    async fn get_one_course_success() {
        let app_state: web::Data<AppState> = web::Data::new(AppState {
            health_check_response: "".to_string(),
            visit_count: Mutex::new(0),
            courses: Mutex::new(vec![]),
        });
        let params: web::Path<(usize, usize)> = web::Path::from((1, 1));
        let resp = get_course_detail(app_state, params).await;
        assert_eq!(resp.status(), StatusCode::OK);
    }
           

如果通過測試,我們将擁有一個完整的具有新增和查詢功能的 api 了,我們将剛剛編寫的路由注冊到我們的主程式:

async fn main() -> io::Result<()> {
    let shared_data = web::Data::new(AppState {
        health_check_response: "I'm OK.".to_string(),
        visit_count: Mutex::new(0),
        courses: Mutex::new(vec![]),
    });
    let app = move || {
        App::new()
            .app_data(shared_data.clone())
            .configure(general_routes)
            .configure(course_routes)
    };

    HttpServer::new(app).bind("127.0.0.1:3000")?.run().await
}
           

現在你可以通過 POSTMAN 等工具來測試新增和查詢資料的 api 了,之後我們将會講解通過資料庫來持久化我們的資料,而不是用全局注入的資料結構存儲資料。

說明

本教程隻是作者的學習筆記,幫助了解和記憶 RUST 開發,課程全部來源是 B站 楊旭老師的教程:

https://www.bilibili.com/video/BV1RP4y1G7KF?p=1&vd_source=8595fbbf160cc11a0cc07cadacf22951

教學 demo 可以檢視大佬的 git:

https://github.com/FrancisYLfan/rust_web_server_from_yang

繼續閱讀