Az előző leckében megismerted, hogyan tudnak async taskok egymással kommunikálni csatornákon (mpsc, oneshot) keresztül. Ez remek alap ahhoz, hogy most egy lépéssel feljebb menjünk: ahelyett, hogy a taskok egymással beszélgetnek egy process-en belül, most azt nézzük meg, hogyan tud a programod a külvilággal – böngészőkkel, más szolgáltatásokkal – beszélgetni HTTP-n keresztül. Ehhez az axum keretrendszert fogjuk használni, amely a Tokio ökoszisztéma egyik legnépszerűbb webszerver-megoldása. Ez a lecke az egész útvonal egyik fordulópontja: innentől kezdve nem csak async kódot írunk, hanem valódi, kívülről elérhető API-t.
Az axum helye a Tokio-ökoszisztémában
Az axum egy async webszerver-keretrendszer, amelyet a Tokio-csapat fejleszt, és amely a Tokio runtime-ra épül. Ez azt jelenti, hogy amit már tudsz a Future-ökről, a .await-ről és a #[tokio::main]-ről, mindet fel fogod tudni használni – az axum nem egy külön világ, hanem egy réteg a Tokio fölött, amely a HTTP-protokoll kezelését és a route-olást (útvonal-illesztést) oldja meg neked.
Ahhoz, hogy axum-mal dolgozhass, két függőségre lesz szükséged a Cargo.toml-ban:
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["full"] }
A tokio "full" feature-je bekapcsolja azokat a képességeket (pl. hálózati I/O, tokio::net::TcpListener), amelyekre egy webszervernek szüksége van – ezeket korábban esetleg nem mind használtad, de a #[tokio::main] attribútum és a tokio::spawn már ismerős neked az előző leckékből.
Az axum verziószámaira mindig érdemes figyelni: a keretrendszer aktívan fejlődik, és a major verziók között API-változások lehetnek. A leckében a 0.8-as vonalat használjuk.
A Router: az útvonalak térképe
Az axum központi fogalma a Router. Ez egy olyan struct, amely leírja, hogy melyik HTTP-metódushoz (GET, POST, stb.) és melyik útvonalhoz melyik handler függvény tartozik. Gondolj rá úgy, mint egy match-re, csak nem egy értéken, hanem a beérkező HTTP-kéréseken – a Router dönti el, hogy egy adott kérést melyik függvénynek adjon oda.
use axum::{routing::get, Router};
async fn hello() -> &'static str {
"Szia, világ!"
}
async fn health_check() -> &'static str {
"OK"
}
fn build_router() -> Router {
Router::new()
.route("/", get(hello))
.route("/health", get(health_check))
}
#[tokio::main]
async fn main() {
let app = build_router();
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("nem sikerült a portra kötni");
axum::serve(listener, app)
.await
.expect("a szerver hibával leállt");
}
A .route(útvonal, metódus(handler)) mintázat a Router API alapköve. A get() függvény az axum::routing modulból egy MethodRouter-t hoz létre, amely azt mondja: "erre az útvonalra GET kéréssel érkező forgalmat ez a handler dolgozza fel". A .route() hívások láncolhatók, így egy Router-en akárhány útvonalat regisztrálhatsz.
A Router::new() visszatérési értéke önmagában is egy érvényes, üres router. Ha csak épp elindítod a szervert egy route nélkül, minden kérésre 404-et fog visszaadni – ez segít abban, hogy a route-hiányzást gyorsan felismerd fejlesztés közben.
Handler-ek: async fn-ek mint HTTP-végpontok
A handler egy közönséges async fn, amelyet a Router hív meg, amikor egy kérés illeszkedik az adott útvonalra. A legegyszerűbb handler nem vár be semmilyen paramétert, és egy olyan típust ad vissza, amely implementálja az axum IntoResponse trait-jét – ez a trait mondja meg az axum-nak, hogyan alakítsa a visszatérési értéket valódi HTTP-válasszá.
A &'static str és a String típusok már "dobozból" implementálják az IntoResponse-t, ezért az alábbi két handler minden további nélkül lefordul és működik:
async fn hello() -> &'static str {
"Szia, világ!"
}
async fn greet_default() -> String {
format!("Szia, ismeretlen felhasználó!")
}
De nem minden típus alkalmas visszatérési értéknek. Ha egy handler olyan típust ad vissza, amelyhez az axum nem tud HTTP-választ generálni, a fordító hibát jelez:
async fn bad_handler() -> i32 {
42
}
fn build_router() -> axum::Router {
axum::Router::new().route("/bad", axum::routing::get(bad_handler))
}
error[E0277]: the trait bound `i32: IntoResponse` is not satisfied
--> src/main.rs:6:44
|
6 | axum::Router::new().route("/bad", axum::routing::get(bad_handler))
| ^^^ the trait `IntoResponse` is not implemented for `i32`
|
= help: the following types implement the trait: ...
Ez a hibaüzenet pontosan jelzi a problémát: az i32-nek nincs olyan implementációja, amely megmondaná az axum-nak, hogyan lesz belőle HTTP-válasz. A gyakorlatban ez azt jelenti, hogy a handler visszatérési típusát mindig valamilyen "válaszként értelmezhető" típusra kell alakítanod – például String-re, vagy (ahogy a következő leckében látni fogod) az axum saját típusaira, mint a Json.
Előretekintés: a handler-ek paramétereket is fogadhatnak (pl. az URL-ből vagy a kérés törzséből kinyert adatokat) – ezt extractoroknak hívják, és ez lesz a következő lecke fő témája.
Az is fontos, hogy a handler egy async fn, tehát belül szabadon használhatsz .await-et – például ha egy másik async függvényt hívsz meg, vagy (ahogy majd egy későbbi leckében látni fogod) adatbázis-lekérdezést futtatsz.
A szerver elindítása: tokio + axum::serve
Ahhoz, hogy a Router-ből valódi, hálózaton figyelő szerver legyen, két lépés szükséges: egy TcpListener-t kell nyitni egy porton, majd az axum::serve függvénynek átadni ezt a listener-t és a Router-t.
#[tokio::main]
async fn main() {
let app = axum::Router::new()
.route("/", axum::routing::get(|| async { "Szia, világ!" }));
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
.await
.expect("nem sikerült a portra kötni");
println!("Szerver fut a 3000-es porton");
axum::serve(listener, app)
.await
.expect("a szerver hibával leállt");
}
Ez a kód pontosan az eddig ismert eszközökkel dolgozik: a #[tokio::main] létrehozza a runtime-ot (ahogy az előző leckékben már megszoktad), a TcpListener::bind(...).await egy async I/O-hívás, amely Result-ot ad vissza (ezért van rajta .expect), és az axum::serve(...).await maga is egy Future, amelyet a runtime futtat – egészen addig, amíg a szervert le nem állítod, vagy hiba nem történik.
Figyeld meg, hogy a Router-be egy closure-t is átadtunk handlerként (|| async { "Szia, világ!" }). Ez ugyanúgy egy async-blokkot visszaadó függvény, mint egy külön definiált async fn, csak helyben, névtelenül írtuk le – kis, egysoros handlerekhez ez praktikus lehet, nagyobb logikánál viszont érdemes külön nevesített async fn-t írni, ahogy az előző szakaszban tettük.
Az axum::serve(...).await sora blokkol (pontosabban: a runtime a Future-t addig futtatja, amíg a szerver le nem áll) – utána írt kód csak leállás után futna le. Ha teszteléshez szeretnéd a szervert egy másik taskban futtatni, a tokio::spawn-t is bevethetnéd, ahogy az korábbi leckében megismertél.
Próbáld ki!
Vegyél egy meglévő (vagy az itt bemutatott) axum-projektet, és adj hozzá egy új route-ot /about útvonalon, GET metódussal, amely egy rövid, saját szöveget ad vissza (pl. a neved és pár szó a projektről). Futtasd a cargo run paranccsal, majd nyisd meg a böngészőben a http://localhost:3000/about címet, és ellenőrizd, hogy a válasz megjelenik-e. Próbálj ki egy nem létező útvonalat is (pl. /nincs-ilyen) – figyeld meg, milyen választ ad az axum alapból.
Önellenőrzés
- Mi a különbség a
Routerés egy handler függvény szerepe között?
Megoldás
A `Router` egy térkép, amely megmondja, hogy melyik HTTP-metódushoz és útvonalhoz melyik handler tartozik – ő végzi a kérések "irányítását". A handler maga egy `async fn`, amely a tényleges munkát végzi el egy adott kérésre, és egy `IntoResponse`-t implementáló típust ad vissza, amelyből az axum HTTP-választ épít.- Miért nem fordul le az a handler, amely
i32-t ad vissza, miközben egy&'static str-et visszaadó handler rendben van?
Megoldás
Az axum minden handler visszatérési típusától megköveteli, hogy implementálja az `IntoResponse` trait-et, mert ez mondja meg, hogyan alakuljon a Rust-érték valódi HTTP-válasszá (státuszkód, törzs, stb.). A `&'static str` és a `String` már rendelkeznek ilyen implementációval, az `i32` viszont nem, ezért a fordító `E0277` hibát jelez trait bound hiánya miatt.- Miért van szükség
#[tokio::main]-re, és mi a szerepe a.await-nek azaxum::servehívásban?
Megoldás
Az `axum::serve(...)` egy `Future`-t ad vissza, amelyet valakinek futtatnia (poll-oznia) kell – ehhez kell egy executor/runtime. A `#[tokio::main]` attribútum gondoskodik arról, hogy a `main` függvény async legyen, és a Tokio runtime elindítsa és futtassa ezt (és a benne lévő összes `Future`-t, beleértve az `axum::serve`-ét is). A `.await` azt jelzi, hogy a `main` task ezen a ponton "átadja a vezérlést" a runtime-nak, amíg a szerver Future-je készen nem áll (vagyis amíg a szerver fut, vagy amíg hiba nem történik).Összefoglalás és továbblépés
- Az axum a Tokio runtime-ra épülő async webszerver-keretrendszer: mindent, amit a
Future-ökről és a#[tokio::main]-ről tudsz, itt is felhasználod. - A
Router::new().route(útvonal, metódus(handler))mintázattal regisztrálhatsz HTTP-végpontokat; a.route()hívások láncolhatók. - A handler egy
async fn, amelynek visszatérési típusa implementálnia kell azIntoResponsetrait-et (pl.&'static str,String) – különben fordítási hibát kapsz. - A szerver elindítása két lépés:
TcpListener::bind(...).await, majdaxum::serve(listener, router).await. - A következő leckében megnézzük, hogyan tudnak a handler-ek paramétereket fogadni extractorokkal (pl. URL-részekből vagy a kérés törzséből), és hogyan lehet megosztott állapotot (state) átadni a handler-eknek.
További olvasmányok: