本教程筆記來自 楊旭老師的 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