LoginSignup
36
14

More than 1 year has passed since last update.

Rustの新しいWebフレームワークaxumのtokio、tower、hyperとの関係を整理する

Last updated at Posted at 2021-09-25

はじめに

RustのWeb周りって複雑ですよね。。さて、2021年7月に新たなWebフレームワークaxumが発表されました。有識者?が「axumがRustのWebフレームワークのデファクトスタンダードになっていくのではないか」と言っている記事を見たので、2021/9月現在のaxumの現状を整理してみました。まずaxumの土台になっているクレート、towerhypertokioについて説明します。

この記事は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

LayerServiceデコレーターです。以下のような定義になっています。

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のリクエストとレスポンスのボディとして使われています。Serverhyperの物が使われています。他にもいろいろ使われているかもしれません。

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フレームワークです。tokiotowerhyperの上で実装されています。(もちろん他にもたくさんのクレートに依存しています。(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

axumtowerの上で実装されているので、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はルーティングできひんのか?」という疑問を持つ方もいらっしゃるかもしれません。安心してください。使えます。このセクションを御覧ください。

 終わりに

この記事ではaxumtokiotowerhyperとの関係性を概観しました。この記事を書いていて、一番懸念したのは「なんか小難しいなあ。Go使えばよくね?」と思われるのではないかということです。確かに自分もほとんどの場面でGoで十分な気がします。ただRustにはゼロコスト抽象化の信念を持っているため、Goより高いパフォーマンスを発揮する可能性がある(Goのランタイムは非常に優秀らしいので、Goに勝つのは大変そうですが。。)、GCによる予測不能な停止がない、メモリ安全性を持っている、代数的データ型やパターンマッチング等、Goにはない機能があるなどの利点が、あることにはあるのでGoより適したユースケースもあるんじゃないかなと思っています。というかあってほしいです。

参考文献

  1. https://github.com/tokio-rs/axum
  2. https://github.com/hyperium/hyper
  3. https://github.com/tower-rs/tower
  4. https://github.com/tower-rs/tower/blob/master/guides/building-a-middleware-from-scratch.md
  5. https://tokio.rs/blog/2021-05-14-inventing-the-service-trait
  6. https://docs.rs/axum/0.2.5/axum/
  7. https://docs.rs/hyper/0.14.13/hyper/
  8. https://docs.rs/tower/0.4.8/tower/
  9. https://zenn.dev/magurotuna/books/tokio-tutorial-ja
36
14
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
36
14