⚙️

Rustで静的ファイルを配布するサーバーを作る

に公開

Rustはとても高速な汎用プログラミング言語です。これを使って静的ファイルを提供するサーバーを作ったら初回起動が速くなりそうなので作ってみましょう!

RustでWebサーバーを実装する方法を知りたい方や、簡単なプログラムでRustを知りたい方を対象としています。

技術選定

RustのWebサーバーを実装できるcrateはいくつかありますが、今回はactix-webを使おうと思います。actix-webはactixというアクターモデルを提供するフレームワーク上に実装されたWebサーバーcrateです。
actix-webはマクロで思ったより簡単にAPIサーバーを作ることができる上にRustのWebサーバーcrateの中で比較的高速な部類なので個人的なお気に入りです!

非同期ランタイム

actix-webは非同期でWebサーバーの処理を行うため、非同期処理ランタイムを入れる必要があります。非同期ランタイムとして有名な実装としてtokioがありますが、今回はactix-webが提供する非同期ランタイムを使用したいと思います。
これを採用する理由は2点あります。

  1. actix-webの一部なので当然actix-webとの親和性が高く非常に高いパフォーマンスが期待できます。今回は速いサーバーを作ることが目的なのでこれに合致します
  2. 今回はtokioである必要がないことも理由の一つです。tokioは非同期処理で必要な機能を網羅的に提供してくれますが、今回やりたいのはファイルをサーブすることだけです。そのため、そこまで多機能である必要がないためactix-webのランタイムを採用します

ファイルのサーブ

actix-webにはactix-filesという機能がありこれを使用することで非常に簡単に静的ファイルをサーブすることができます。

実装

ということで実装を始めるため、プロジェクトを作りましょう!

cargo new static-files-server

依存の追加

以下のコマンドで依存を追加します

cargo add actix-web actix-files

これを実行するとCargo.tomlは以下のようになると思います。

[package]
name = "static-files-server"
version = "0.1.0"
edition = "2024"

[dependencies]
actix-files = "0.6.8"
actix-web = "4.11.0"

各依存のバージョンは実行したタイミングで変化しますので、これらの依存が入っていれば問題ありません。

非同期ランタイムを入れる

作られたプロジェクトのsrc/main.rsを開いてみましょう!

fn main() {
    println!("Hello, world!");
}

このような簡単なhello worldプログラムが作られていると思います。
非同期処理に対応するためこちらを以下のように修正しましょう!

#[actix_web::main]
async fn main() {
    println!("Hello, world!");
}
tokioを使用する場合

tokioを使用する場合は#[actix_web::main]の部分を#[tokio::main]に変えることで使用できます

#[tokio::main]
async fn main() {
    println!("Hello, world!");
}

Webサーバーを実装する

まず以下のコードを見てください。

use actix_web::{App, HttpServer};
use actix_files::Files;

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    HttpServer::new(|| {
        App::new().service(Files::new("/", "./path/to/files"))
    })
    .bind(("127.0.0.1", 8080))?
    .run()
    .await
}

こちらのコードはactix-webとactix-filesを使用し./path/to/filesにあるファイルを/配下でサーブするWebサーバーです。

一つずつ説明していきます。
まずHttpServerはactix-webのサーバーインスタンスです。このサーバーはデフォルトでCPU分だけワーカーを起動し処理を行います。
この中に書かれているAppはサーバーの設定です。ここでログの出力をしたり、APIなどのエンドポイントを設定したりします。今回はactix-filesのFilesを設定しています。
次にbindですが、127.0.0.1はおなじみlocalhostのアドレスで自分のホスト自身を指します。ポートは8080になっています。
HTTPではデフォルトで80番を使用しますが1023以下はsystem ports (昔はwell-known ports) と呼ばれ多くのOSで特権がないとbindできないアドレスです。そのため今回は8080を使用しています。(あまりにも80の代わりとして8080が使われるのでhttp-altという名前がついていたりします。)

これで一応静的ファイルをサーブできるようになったのでこれで終わりでも良いですが、今回はもう少しリッチにしましょう!

コマンドライン引数を受け取る

まず、コマンドライン引数パーサーを入れます。

cargo add clap --features derive

clapはRustで使用できる非常に便利なコマンドライン引数パーサーです。今回はderiveマクロで便利に記述したいのでderive featureを入れています。

引数として受け取る内容を決めましょう。
まず待ち受けるサーバーのアドレスとポートです。デフォルトは127.0.0.1:8080で良いですが、時には変更したいこともあるでしょう。
次にファイルのあるディレクトリへのパスです。

use actix_web::{App, HttpServer};
use actix_files::Files;
use clap::Parser;

#[derive(Parser)]
struct Args {
    #[arg(short, long, help = "Listen address and port", default_value = "127.0.0.1:8080")]
    listen: String,

    #[arg(short, long, help = "Path to static files directory")]
    directory: String,
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    let args = Args::parse();

    HttpServer::new(move || {
        App::new().service(Files::new("/", &args.directory))
    })
    .bind(args.listen)?
    .run()
    .await
}

以上が書き換え後のソースファイルです。
まずArgsはコマンドライン引数の設定です。これに#[derive(Parser)]をつけることでパースができるようになります。

#[arg(short, long, help = "Listen address and port", default_value = "127.0.0.1:8080")]
listen: String,

argは引数の定義です。shortはショートハンドとして一文字目だけで指定できます。例えばlistenという項目は-l <ADDR>--listen <ADDR>は同じ意味となります。
help--helpとして実行した際に表示されるヘルプです。
default_valueは指定されたなかった場合のデフォルト値を指定できます。

次にArgs::parse()ではコマンドライン引数をパースします。--helpは勝手に処理されるので何もする必要はありません。

次にHttpServerについてです。
moveがクロージャーについていますが、これはargsをクロージャー内で必要なためです。また気をつけないといけないのが、今回は&args.directoryと参照を渡している部分です。HttpServerに渡すクロージャーはワーカーごとに呼び出されるため何度も呼ばれることを考慮しなければいけません。(FnOnceではなくFnであるということですね)
もしここで、args.directoryと値そのものを渡してしまうと、所有権をFiles::newに渡してしまうことになるため2回目以降は所有権が失われてしまうためコンパイルエラーになってしまいます。

ログ

最後にログをつけましょう!
actix-webは標準でロガー機能がついているのでアクセスログを残すことができます。

cargo add env_logger

このコマンドでロガーを依存に追加します。
このロガーはRUST_LOGという環境変数でログレベルを変えることができる非常に簡単なロガーです。

use actix_web::{App, HttpServer};
use actix_web::middleware::Logger;
use actix_files::Files;
use clap::Parser;

#[derive(Parser)]
struct Args {
    #[arg(short, long, help = "Listen address and port", default_value = "127.0.0.1:8080")]
    listen: String,

    #[arg(short, long, help = "Path to static files directory")]
    directory: String,
}

#[actix_web::main]
async fn main() -> std::io::Result<()> {
    env_logger::init();

    let args = Args::parse();

    HttpServer::new(move || {
        App::new()
            .wrap(Logger::default())
            .service(Files::new("/", &args.directory))
    })
    .bind(args.listen)?
    .run()
    .await
}

env_loggerinitを呼び出し初期化を行います。
アクセスログは.wrap(Logger::default())の部分で収集できます。wrapはミドルウェアという機能を追加する機能で、リクエストとレスポンスの処理に割り込むことができる機能です。Loggerはそれを利用してログを残すミドルウェアです。

これでログが出て、指定したディレクトリのファイルをサーブができるサーバーができました!

Discussion