Rustで使いまわしの良いactix-web製HTTPサーバモジュールの基礎を作る

目的

必要にかられてRustでHTTPサーバを実装することになりました。

HTTPサーバは何かと使い勝手が良く、今後も気軽に立てたくなることがあるはずなので、ここで一つすごくシンプルなHTTPサーバのモジュールを書いてみました。

せっかくなのでRustが誇る高速なHTTPサーバであるactix-webを使い、startとstopのインターフェイスを持たせました。

正直言うと、actix-webのexamplesのレポジトリにはとても参考になるソースコードがたくさんあるので、私もそれを使って実装しています。皆さんもactix-webを使うときには、このレポジトリを参考にしたら良いと思います。

ただ、GoogleでHTTPサーバを調べたときに、もっとお手軽なソースコードがヒットしてもよかろう、という気持ちがあり、この記事を書くことにしました。

使用したRustのバージョンは、1.40。

ソースコード

Cargo.toml

dependenciesのところだけ抜粋しています。 actix-webと、そのランタイムを扱うactix-rtを使いました。

[dependencies]
actix-web = "2.0.0"
actix-rt = "1.0.0"

http_server.rs

使っている構造体などがどのcrate由来のものなのかわかるように、あえてuseを使わずに書いています。

エラーハンドリングはほとんどunwrap()しています。

pub struct HTTPServer {
    server: Option<actix_web::dev::Server>,
}

impl HTTPServer {
    pub fn new() -> HTTPServer {
        HTTPServer { server: None }
    }

    pub fn start(&mut self) {
        self.stop();

        let (tx, rx) = std::sync::mpsc::channel();

        std::thread::spawn(move || {
            let mut system = actix_rt::System::new("start_server");
            let server = actix_web::HttpServer::new(|| {
                actix_web::App::new()
                    .service(
                        actix_web::web::resource("/ok").to(|req: actix_web::HttpRequest| {
                            println!("{:?}", req);
                            actix_web::HttpResponse::Ok().body("ok")
                        }),
                    )
                    .service(
                        actix_web::web::resource("/ng").to(|req: actix_web::HttpRequest| {
                            println!("{:?}", req);
                            actix_web::HttpResponse::Ok().body("ng")
                        }),
                    )
            })
            .bind("0.0.0.0:8888")
            .unwrap()
            .run();
            tx.send(server.clone()).unwrap();
            system.block_on(server).unwrap();
        });

        self.server = Some(rx.recv().unwrap());
    }

    pub fn stop(&mut self) {
        if let Some(server) = &self.server {
            let mut system = actix_rt::System::new("stop_server");
            system.block_on(server.stop(true));
        }
        self.server = None;
    }
}

main.rs

mod http_server;

fn main() {
    let mut server = http_server::HTTPServer::new();

    server.start();

    std::thread::sleep(std::time::Duration::from_secs(10));

    server.stop();
}

解説

HTTPServerをstart()させるときには、裏で勝手に動き出してほしいので、別スレッドでactix-webのHTTPサーバを起動させています。

actix_web::HttpServer::new()などでインスタンスを作成できますが、サーバを止めたいときには再びこのインスタンスを使う必要があります。

そのため、HTTPServer構造体の中に保存できるようにします。OptionでServerを包み、HTTPServer構造体をnew()したときには中身がNone、start()したらインスタンスを入れられるようにしました。

pub struct HTTPServer {
    server: Option<actix_web::dev::Server>,
}

ここで問題になるのが、スレッド内で作ってしまったインスタンスをどうやってメンバ変数self.serverに入れるのかです。

今回は、std::sync::mpsc::channel()を使って、スレッドの中から外へのチャンネルを作成しています。 この抜け道を辿って、サーバのインスタンスをスレッドの外へ逃し、HTTPServer構造体のメンバ変数として保存しています。 (The Bookのこちらの記事が参考になります。)

また、今回のHTTPServerを実装する上で、呼び出す側が「asyncを付けなきゃいけない」ということを意識させたくない作りにしたかったので、awaitは使わず、system.block_on(server)しています。

main.rsでは10秒間だけ動くHTTPServerを実装しています。

受け付けるHTTPリクエストのパスは「/ok」「/ng」だけ実装しました。 ここは、その時々に応じて修正しましょう。 呼ばれるパスごとに関数を用意したり、テンプレートエンジンを使うのも良いでしょう。

まとめ

これを使いまわせば、スムーズにHTTPサーバが作れます。

もう少し複雑なことでも、actix-webのexamplesのレポジトリにはとても参考になるソースコードがたくさんあるので、このレポジトリを参考にすると良いです。

f:id:toyamaguchi:20191221150338p:plain