はじめに
RustのWeb周りって複雑ですよね。。さて、2021年7月に新たなWebフレームワークaxum
が発表されました。有識者?が「axumがRustのWebフレームワークのデファクトスタンダードになっていくのではないか」と言っている記事を見たので、2021/9月現在のaxumの現状を整理してみました。まずaxumの土台になっているクレート、tower
、hyper
、tokio
について説明します。
この記事はRustの文法をある程度知っていることを前提にしています。
tower
以下で書かれているのはtower 0.4.8時点での情報です。
tower
は「リクエストを受け付けて、いつかレスポンスを返す物」を実装するための土台を用意しています。代表的なものはHTTPですが、他のプロトコルも実装可能です。また、タイムアウトや流量制限、バッファリングなどのミドルウェアの実装を提供しています。
tower
は以下のようなトレイトを提供しています。
towerが提供するトレイト
Service
Service
はリクエストを受け取って、レスポンスを生成する機能を提供するトレイトです。これがtower
の核です。以下のような定義になっています。
trait Service<Request> {
type Response;
type Error;
type Future: Future<Output = Result<Self::Response, Self::Error>>;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>>;
fn call(&mut self, Request) -> Self::Future;
}
call関数がリクエストを処理する関数です。callはSelf::Future
つまり、Future<Output = Result<Self::Response, Self::Error>>
を返します。つまり成功すれば、Self::Response
、失敗すればSelf::Error
を返すFuture
です。response
がジェネリクスではなく、関連型なのは、ある構造体が、あるリクエストに対して関連するレスポンスは1つにしたいからです。関連型とジェネリクスの違いについてはこの記事が参考になります。
poll_readyはリクエストを処理する準備ができているかどうかを返す関数です。
Layer
Layer
はService
のデコレーターです。以下のような定義になっています。
trait Layer<S> {
type Service;
fn layer(&self, inner: S) -> Self::Service;
あるミドルウェアがLayer
トレイトを実装している場合、layer関数を呼ぶことで、サービスにミドルウェアの機能を付加した新しいサービスを生成することができます。現時点でtowerに用意されているミドルウェアは、少なくとも私が見たものはすべてLayer
トレイトを実装していました。ただし、ServiceBuilder
を使えば、Layer
トレイトを実装していなくても、サービスに機能を付加することは可能です。
ServiceBuilder
Service
のビルダーです。ServiceBuilder
を用いることで以下のようにサービスを生成することができます。
ServiceBuilder::new()
.concurrency_limit(10)
.buffer(100)
.service(svc);
hyper
hyper
は低レベルのHTTPライブラリです。axum
ではhyperのBody構造体がHTTPのリクエストとレスポンスのボディとして使われています。Server
もhyper
の物が使われています。他にもいろいろ使われているかもしれません。
tokio
tokio
はRustの非同期ランタイムです。RustはGoのようにグリーンスレッドをスケジューリングするランタイムは言語に付属していません。したがって、外部クレートを用いる必要があります。Rustの非同期処理というのは、FutureをExecutorという実行器に渡すことでTaskを生成し、ExecutorがそのTaskをスケジューリングして実行するといった流れで行われます。tokioはRustで非同期処理を実行するためのランタイムを提供しています。
Future、Executor、Task。。なんそれ?という人はtokioのチュートリアルを和訳してくれている方がいるので、これを読んで見ると良いと思います。
axum
以下で書かれているのはaxum 0.2.5時点での情報です。
axumはRustのWebフレームワークです。tokio
、tower
、hyper
の上で実装されています。(もちろん他にもたくさんのクレートに依存しています。(ex. tower-http
等)
最初にサンプルプログラムをお見せします。{"first_name":"Jeff", "last_name":"Dean", "age":58}
のようなJSONをPOSTで投げると、<p>Hello, Jeff Dean! 58 years old! Looks young!</p>
のようなレスポンスをContent-Type: text/html
で返すサーバーです。
use axum::{
handler::post,
Router,
response::Html,
Json,
};
use tokio::time::Duration;
use tower::timeout::TimeoutLayer;
use serde::Deserialize;
#[derive(Deserialize, Debug)]
struct User {
first_name: String,
last_name: String,
age: u32,
}
#[tokio::main]
async fn main() {
// Routerはhandlerとserviceを合成するための構造体です。
// towerのミドルウェア、Timeoutを使いタイムアウト機能を付加しています。
let app = Router::new().route("/", post(handler)).layer(TimeoutLayer::new(Duration::from_secs(1)));
// hyperのServerでappをserviceに変換したものを実行します。
axum::Server::bind(&"0.0.0.0:3000".parse().unwrap())
.serve(app.into_make_service())
.await
.unwrap();
}
// Json<User>がExtractor、Html<String>がIntoResponseを実装している構造体
async fn handler(Json(user): Json<User>) -> Html<String> {
// ほんとはサニタイズしないとアカンよね。。
Html(format!("<p>Hello, {} {}! {} years old! Looks young!</p>", user.first_name, user.last_name, user.age))
}
以下のような概念が存在します。
handlers
Requestを処理してResponseを返す関数です。正確に言うと、0個以上のExtoractors
を引数にとり、IntoResponse
を実装しているなにかを返す非同期の関数です。
Extractors
FromRequest
を実装している型です。つまりリクエストから変換できる型ということです。handlerの引数として使われます。
Response
IntoResponse
を実装している物はhandlerの返り値になることができます。
Middleware
axum
はtower
の上で実装されているので、tower
のミドルウェアを使うことができます。サンプルプログラムではタイムアウト機能を付加しています。ハンドラ内にsleep関数を書き、ワザとタイムアウトするようにして、httpieでpostリクエストを送ってみたところ、
http: error: ConnectionError: ('Connection aborted.', RemoteDisconnected('Remote end closed connection without response')) while doing a POST request to URL: http://localhost:3000/
というエラーになりました。(ほんとはサーバー側でエラーハンドリングしなきゃだめっぽいですが。。)
Serviceのルーティング
「axumはtowerの上に実装されているんやから、serviceはルーティングできひんのか?」という疑問を持つ方もいらっしゃるかもしれません。安心してください。使えます。このセクションを御覧ください。
# 終わりに
この記事ではaxum
のtokio
、tower
、hyper
との関係性を概観しました。この記事を書いていて、一番懸念したのは「なんか小難しいなあ。Go使えばよくね?」と思われるのではないかということです。確かに自分もほとんどの場面でGoで十分な気がします。ただRustにはゼロコスト抽象化の信念を持っているため、Goより高いパフォーマンスを発揮する可能性がある(Goのランタイムは非常に優秀らしいので、Goに勝つのは大変そうですが。。)、GCによる予測不能な停止がない、メモリ安全性を持っている、代数的データ型やパターンマッチング等、Goにはない機能があるなどの利点が、あることにはあるのでGoより適したユースケースもあるんじゃないかなと思っています。というかあってほしいです。
参考文献
- https://github.com/tokio-rs/axum
- https://github.com/hyperium/hyper
- https://github.com/tower-rs/tower
- https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md
- https://tokio.rs/blog/2021-05-14-inventing-the-service-trait
- https://docs.rs/axum/0.2.5/axum/
- https://docs.rs/hyper/0.14.13/hyper/
- https://docs.rs/tower/0.4.8/tower/
- https://zenn.dev/magurotuna/books/tokio-tutorial-ja