フレームワークなしでミニマリストなRust HTTPサーバーを構築する
Ethan Miller
Product Engineer · Leapcell

活気のあるRustエコシステムでは、Axum、Actix-web、Warpなどの多数のWebフレームワークが、堅牢なHTTPサービスを構築するための簡素化されたパスを提供します。これらのフレームワークは、便利な抽象化、実績のあるミドルウェア、および広範な機能セットを提供し、開発を大幅に加速します。しかし、HTTPサーバーの基本的なメカニズムを理解したい人、または極端な制御と最小限の依存関係を要求するプロジェクトにとっては、これらのフレームワークをバイパスしてコアライブラリで直接構築することが非常に価値のあるものになります。この探求は、HTTPプロトコルにhyper、非同期ランタイムとI/Oにtokioを活用して、ミニマリストRust HTTPサーバーを構築するプロセスを解き明かします。このような取り組みは、Rustにおける非同期プログラミングの理解を深めるだけでなく、コアライブラリのパワーと柔軟性を示すことで、効率的で高度にカスタマイズ可能なソリューションにつながります。
Raw HTTPサーバーのコアコンポーネント
コーディングに入る前に、使用する主要なコンポーネントを簡単に定義しましょう。
tokio: Rustのための強力な非同期ランタイムです。非ブロッキングI/O、タスクスケジューリング、およびfuturesの管理に必要なツールを提供します。私たちのサーバーでは、tokioは着信接続のリスニングと同時リクエストの効率的な管理を処理します。hyper: Rustで書かれた高速で正確なHTTP実装です。hyperはWebフレームワークよりも低レベルで動作し、HTTP/1.1およびHTTP/2のリクエストとレスポンスを直接扱います。HTTPプロトコル自体の複雑さを抽象化しますが、アプリケーションのリクエスト処理ロジックは抽象化しないHTTPリクエストの解析と、HTTPレスポンスの送信バイトストリームへのフォーマットを処理します。async/await: Rustに組み込まれた非同期コードを記述するための構文です。同期コードのように見え、感じられる非ブロッキングコードを書くことができ、非同期プログラミングをより人間工学的にします。
これらのピースがどのように連携するかを理解することは不可欠です。tokioは非同期実行環境とTCP接続を受け入れる手段を提供します。接続が確立されると、hyperがバイトストリームをHTTPリクエストに解析し、次にHTTPレスポンスをバイトストリームにシリアライズしてネットワーク経由で送信する処理を引き継ぎます。私たちのアプリケーションロジックは、この2つのステップの間に位置し、解析されたリクエストを処理し、適切なレスポンスを生成します。
ミニマリストサーバーの構築
まず、必要な依存関係をCargo.tomlに設定しましょう。
[package] name = "minimal-http-server" version = "0.1.0" edition = "2021" [dependencies] tokio = { version = "1", features = ["full"] } # "full"にはランタイム、ネット、マクロなどが含まれます hyper = { version = "0.14", features = ["full"] }
次に、サーバーのRustコードを記述します。目標は、127.0.0.1:3000をリッスンし、すべてのリクエストに単純な"Hello, world!"メッセージで応答するサーバーを作成することです。
use std::{convert::Infallible, net::SocketAddr}; use hyper::{Body, Request, Response, Server}; use hyper::service::{make_service_fn, service_fn}; // コアとなるリクエストハンドラ関数 async fn handle_request(_req: Request<Body>) -> Result<Response<Body>, Infallible> { // このシンプルな例では、着信リクエストは無視します Ok(Response::new("Hello, world!".into())) } #[tokio::main] async fn main() { // リッスンするアドレスを定義します let addr = SocketAddr::from(([127, 0, 0, 1], 3000)); // `Service`は、RequestからResponseへの非同期関数を表すトレイトです。 // `make_service_fn`は、各着信接続に対して新しい`Service`を作成するために使用されます。 let make_svc = make_service_fn(|_conn| async { // `service_fn`は、`Fn`を`Service`に変換するためのヘルパーです Ok::<_, Infallible>(service_fn(handle_request)) }); // 指定されたアドレスとサービスファクトリでサーバーを作成します let server = Server::bind(&addr).serve(make_svc); println!("Listening on http://{{}}", addr); // エラーが発生するまでサーバーを実行します if let Err(e) = server.await { eprintln!("server error: {{}}", e); } }
このコードを分析しましょう。
useステートメント:std、hyper、hyper::serviceから必要な型をインポートします。Infallibleは、Errバリアントが決して発生しないResult型に使用され、基本的な例のエラーハンドリングを簡素化します。handle_request関数: このasync関数は、サーバーのコアロジックです。Request<Body>を入力として受け取り、Result<Response<Body>, Infallible>を返します。この例では、着信リクエストを無視し、常に"Hello, world!"をボディに持つHTTP 200 OKレスポンスを返します。#[tokio::main]属性:tokioのマクロは、Tokioランタイムの設定を簡素化します。main関数を非同期エントリポイントに変換し、ランタイムの初期化を処理します。SocketAddr::from(([127, 0, 0, 1], 3000)): これは、サーバーがリッスンするIPアドレスとポートを定義します。make_service_fn: これはhyperサーバーにとって重要な関数です。各新しいTCP接続に対してServiceインスタンスを返すクロージャを受け取ります。このパターンにより、接続固有の状態が必要な場合(この単純なケースではそうではありません)が可能になります。そのクロージャもasyncである必要があります。service_fn(handle_request):make_service_fn内で、service_fnは、私たちのhandle_requestasync関数をhyperのServiceトレイト実装に適合させるために使用されます。これはサーバーが期待するものです。Server::bind(&addr).serve(make_svc): この行はhyperサーバーを作成および設定します。bindはリッスンするアドレスを指定し、serveは着信接続を処理するためにmake_svcファクトリを受け取ります。server.await: これはサーバーを開始します。awaitキーワードは、サーバーが正常にシャットダウンするかエラーが発生するまでmainの実行を一時停止します。
実行とテスト
このサーバーを実行するには、ターミナルでプロジェクトディレクトリに移動し、次を実行します。
cargo run
Listening on http://127.0.0.1:3000と表示されるはずです。次に、Webブラウザまたはcurlのようなツールを使用してサーバーにアクセスします。
curl http://127.0.0.1:3000
Hello, world!というレスポンスを受け取るはずです。
このシンプルな例は、基本的な構成要素を示しています。handle_requestを拡張して、以下を実行できます。
- リクエストパスの解析: 
req.uri().path() - リクエストヘッダーの読み取り: 
req.headers() - リクエストボディの読み取り: 
hyper::body::to_bytes(req.into_body()).await - 動的レスポンスの生成: 異なるステータス、ヘッダー、ボディコンテンツを持つ
Response<Body>を構築します。 
結論
hyperとtokioを直接利用することで、高レベルフレームワークに依存せずに、RustでミニマリストHTTPサーバーを正常に作成しました。この実践的なアプローチは、非同期プログラミング、HTTPプロトコル、およびパフォーマンスの高いネットワークサービスを構築する際に伴うアーキテクチャ上の選択について、深い洞察を提供します。これは、Rustが安全性と並行性を維持しながら高性能で低レベルのネットワークアプリケーションを提供する能力を強調し、開発者がゼロからカスタムで効率的なサーバーソリューションを構築できるようにします。