在開始之前先快速介紹如何初始化一個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使用