在開始之前先快速介紹如何初始化一個Rust專案:
//// 初始化一個binary專案,也可省略--bin
cargo init hello_world --bin
//// 這會生成以下的檔案結構
//// .
//// ├── Cargo.toml
//// └── src
//// └── main.rs
cargo init初始化了一個名為hello_world的binary專案,如果要初始化函式庫專案,則需要使用--lib選項,這樣src目錄下會生成一個lib.rs檔案。打開Cargo.toml檔案會看到以下內容:
[package]
name = "hello_world"
version = "0.1.0"
edition = "2021"
[dependencies]
在Cargo專案會使用toml文件來管理專案的依賴。目前這是一個空的專案,因此[dependencies]區段是空白的。接下來需要添加Axum的依賴。請在terminal中輸入以下命令:
cargo add axum tokio --features tokio/full
這時候就會看到Cargo.toml的[dependencies]下多了幾行:
[dependencies]
axum = "0.6.20"
tokio = { version = "1.32.0", features = ["full"] }
也可以透過直接編輯toml檔的方式將依賴加到專案裡面。根據官方文件,因為依賴於tokio這個非同步crate,所以我們也要一併把tokio加進去,注意一下features,由於rust的crate下可能有多個功能,可以透過指定features來使用所需要的功能。
在此先研究一下官方的第一個範例,打開main.rs
use axum::{
routing::get,
Router,
};
#[tokio::main]
async fn main() {
// build our application with a single route
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
// run it with hyper on localhost:3000
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
如果有打開看過一開始使用cargo init產生的main,會發現和範例中長的不太一樣。範例中的main除了前面多了async修飾詞外,也多了[tokio::main],這是rust的巨集功能,透過標注告知編譯器在編譯時會需要多生成一些程式碼,我們先來安裝cargo的擴充功能cargo-expand,透過它可以將巨集展開。
cargo install cargo-expand
安裝完畢後下達指令
cargo expand
接下來就會看到將[tokio::main]巨集展開的結果
#![feature(prelude_import)]
#[prelude_import]
use std::prelude::rust_2021::*;
#[macro_use]
extern crate std;
use axum::{routing::get, Router};
fn main() {
let body = async {
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
};
#[allow(clippy::expect_used, clippy::diverging_sub_expression)]
{
return tokio::runtime::Builder::new_multi_thread()
.enable_all()
.build()
.expect("Failed building the Runtime")
.block_on(body);
}
}
本質上仍然是一個普通的同步的main,先來看最後return的部份,前面的大意是準備一個tokio多執行序的非同步runtime,注意一下這邊使用了builder pattern,這是rust中的一個特色,在很多crate中都會使用這個模式。當runtime準備好後調用了block_on方法,並把原本寫在main的內容以非同步閉包的方式傳進來。在非同步的模型中,rust需要一個基底的程序來負責調度所有的非同步任務,block_on會以執行緒阻塞的方式來處理傳進來的非同步方法,而由外面看起來就像一個同步方法一樣。expect則是rust中錯誤處裡的方式,這邊表示如果runtime準備失敗的話就會中止程式並告知在準備Runtime時發生錯誤,之後有空會特別介紹rust中的錯誤處裡。
接下來,讓我們看一下router和server的部分:
let app = Router::new().route("/", get(|| async { "Hello, World!" }));
在這段程式碼中Router::new()產生了一個結構體,用於定義Web應用程序的路由和路由處理程序,作用是將不同endpoint的請求分派給相應的handler。handler是一個處理函數,在這個範例中是個非同步的closure,透過這樣的方式就可以簡潔的設定路由與處裡請求的邏輯。
這邊我想與C#的MVC比較一下,在.Net還沒有出現minimal api以前,我們通常需要建立一個Controller的類別,然後在該類別中定義多個操作方法(action methods),這點rust就和C#十分不同,rust並不是一門標榜物件導向的語言,在rust中,方法並不需要依附於某個class之下。
然後是Server的部份:
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
這個部份是運行應用程式的關鍵,透過監聽3000這個port,並將請求分派到app相應的處理器上。我想強調一下into_make_service這個方法,Rust社群許多crate的設計都非常的模組化,以Axum來說只專注於解析API的Request與Response,本身只提供少量的middleware。rust的世界中有另外一個叫做tower的crate專們處裡非同步的請求與回應,service是tower crate中一個核心的抽象概念,而into_make_service這個方法的目的是要將axum的應用程式包裝成一個service以便嵌入到tower的系統中。
tower生態系中提供了很多的中介軟體來協助處裡請求與響應,而axum因為支援tower,也就可以使用生態系中大多數的組件,明天預計對router與tower進行更深入的介紹。
註:Crate是rust套件系統中編譯的最小單位,在這邊簡單理解成其他語言的套件或函式庫即可,可以到Crates.io找到自己需要的crate使用