RustでWebサーバーを立てるためのフレームワークとしてaxumがあります。
そんなaxumの使い方を通してRustの構文などに触れていきたいと思います。
(某所でのLTの発表資料のままです…。)
もくじ
- axumとは?
- axumでどうやってサーバーを立てるのか?
- ハンドラーの実装の仕組み
- (おまけ)
そもそもaxumとは?
Rust製のウェブアプリケーションフレームワーク。
基本的にはhttpサーバーを作るためのライブラリ。
httpサーバーを作るためには
(アプリケーション層では)基本的にはこの機能が出来れてれば十分
- http リクエストを受け取る
- ルーティング
- リクエストからレスポンスを作る
- http レスポンスを送る
axumでhttpサーバーを作るためには
axumでは何が必要か
- http リクエストを受け取る -> axum(hyper+tower)が用意
- ルーティング -> axumが機能を提供
- リクエストからレスポンスを作る -> axum(tower)が機能を提供
- http レスポンスを送る -> axum(hyper)が用意
axumでhttpサーバーを作るためには
- ルーティング
- リクエストからレスポンスを作る(関数を作る)
この二つをやればいい!
axumで実装するには?
let app = Router::new()
.route("/", get(root));
async fn root() {}
// run our app with hyper, listening globally on port 3000
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
これだけ!
ルーター
このパスに来たらどんな関数を実行するかを登録する
// our router
let app = Router::new()
.route("/", get(root))
.route("/foo", get(get_foo).post(post_foo))
.route("/foo/bar", get(foo_bar));
ハンドラー
リクエストに対してどんなレスポンスを返すかの関数のこと。
async fn get_foo(
header: HeaderMap
query: Query<GetFooQuery>,
body: Json<GetFooRequestBody>,
) -> Result<(StatusCode, HeaderMap, Json<GetFooResponseBody>)> {
/**/
}
リクエストとして必要な情報を引数に書き、
レスポンスとして返却するものを戻り値にするだけ!
関数を登録するだけで機能するのは?
ある一定のルールを満たした関数なら何でもハンドラーとして登録できる。
-> どうして?
traitをうまく使っているから!
getの実装
pub fn get<H, T, S>(handler: H) -> MethodRouter<S, Infallible>
where
H: Handler<T, S>,
T: 'static,
S: Clone + Send + Sync + 'static,
getの実装(概略)
pub fn get<H>(handler: H) -> MethodRouter<_, _>
where
H: Handler<_, _>,
H
は型パラメータで、型自体がパラメータとして入る
whereにはH
が型として満たすべきものを指定している
Handler(1) Handlerの定義
pub trait Handler<T, S>: Clone + Send + Sized + 'static {
type Future: Future<Output = Response> + Send + 'static;
// Required method
fn call(self, req: Request, state: S) -> Self::Future;
// Provided methods
fn layer<L>(self, layer: L) -> Layered<L, Self, T, S>
where L: Layer<HandlerService<Self, T, S>> + Clone,
L::Service: Service<Request> { ... }
fn with_state(self, state: S) -> HandlerService<Self, T, S> { ... }
}
Handler(2) Handlerの定義(概略)
pub trait Handler<T, S>: Clone + Send + Sized + 'static {
// Required method
async fn call(self, req: Request, state: S) -> Response;
}
traitは基本的には関数の集合
Handler traitを名乗るには、callという関数を用意することを要求している
Handler(3) Handlerの実装(概略)
axumではHandlerのimplを 型 F に対して定義している
impl<T1, T2> Handler<(T1, T2)> for F
{
fn call(self, req: Request, state: S) -> Future { ... }
}
Handler(4) Handlerの実装(概略)
axumではHandlerのimplを FnOnceを実装しているGenerics に対して定義している
impl<F, Fut, T1, T2> Handler<(T1, T2)> for F
where F: FnOnce(T1, T2) -> Fut,
{
fn call(self, req: Request, state: S) -> Self::Future { ... }
}
FnOnce
関数やクロージャーは実体のように扱うことができる。
fn function_1() { ... }
let x = function_1;
let y = || {} // クロージャー
条件によって、自動的にFnOnce
, FnMut
, Fn
が実装(impl)され、
関数であるという制約として使うことができる
(これらだけは制約の表現が若干異なる)
Handler(4) trait制約
実際の定義→ 引数と戻り値に制約がある
impl<F, Fut, S, Res, M, T1, T2> Handler<(M, T1, T2), S> for F
where
F: FnOnce(T1, T2) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Res> + Send,
S: Send + Sync + 'static,
Res: IntoResponse,
T1: FromRequestParts<S> + Send,
T2: FromRequest<S, M> + Send,
{/**/}
Handler(5) FromRequest
最後の引数はBodyから得られる情報を受け取るFromRequest
が、
それ以外の引数は、その他のFromRequestParts
が要求される
FromRequest
: Bytes
, Json<T>
等
FromRequestParts
: HeaderMap
, Method
, Uri
, Query<T>
, Version
等
Handler(6) serde
Json<T>
型は具体的なJsonをデシリアライズした状態で受け取れる
#[derive(Deserialize)]
pub struct FooRequestBody {
name: String,
id: i64,
}
async get_foo(body: Json<FooRequestBody>) -> String {/**/ }
{ "name": "aaa", "id": 10 }
のようなjsonを受け入れるようになる
Handler(7) IntoResponse
Response
を作れる状態の型に定義される
Json<T>
, Html<T>
, Redirect
, Bytes
, StatusCode
等
Response
自体もimplしているので自力で作ってもいい
Handler(8) Future
非同期的に返すための値
最初はasync fn
にしておけば気にしなくていい
まとめ
Handlerは大抵のリクエスト、レスポンスの形に合わせて、traitで抽象化して、関数を登録できるようになっている。
rust docを参考にするとどの型が何を実装しているかが見える
(おまけ)Handler実装で詰まったときは
詰まる例(1) FromRequestの場所が最後以外
Handler
のimplは、FromRequest
が最後の引数のときだけに受け入れる
順番が逆になっているとコンパイルエラーになる
impl<F, Fut, S, Res, M, T1, T2, T3> Handler<(M, T1, T2, T3), S> for F
where
F: FnOnce(T1, T2, T3) -> Fut + Clone + Send + 'static,
Fut: Future<Output = Res> + Send,
S: Send + Sync + 'static,
Res: IntoResponse,
T1: FromRequestParts<S> + Send,
T2: FromRequestParts<S> + Send,
T3: FromRequest<S, M> + Send,
詰まる例(2) FromRequestになっていない
Json<T>
はT: DeserializeOwned
が要求されている
そのためにはT
がライフタイムを持ってはいけない
#[derive(serde::Deserialized)]
struct NotOwned<'a> {
borrowed_str : &'a str,
owned_string: String,
}
詰まる例(3) Sendが消える
Fut: Future<Output = Res> + Send,
基本的には非同期関数を通常通り書いていればSendになるが…
使うとSend
が消えるものがある
Rc
とか、rand::rngs::ThreadRng
とか、!Send
になっているものを使うとasyncが!Send
になる
(おまけ)ステップアップするなら学ぶこと
Service
axumは、towerというClient,Serverを作るcrateの上に乗っかているので、
Service
についてみておくとaxumがどのような原理で動いているかわかる。
Middleware
これもtowerのLayerの上に成り立っている。
リクエストのフィルター、ログ、認証など
汎用的に拡張できるので学んでおくといいかも
Extractor
とかも知ることになる