全3663文字
PR

 今月はRustの非同期処理をHTTPサーバーの開発を通して説明している。実際に手を動かすことで、Web開発の基本であるHTTPサーバーの仕組みと非同期処理の基礎を同時に学ぶことができる。

並行サーバーの実装

 HTTPサーバーには、クライアントからリクエストが来た順番に処理する「反復サーバー」とイベントを切り替えながらリクエストを処理する「並行サーバー」がある。今回は、反復サーバーを基に並行サーバーを実装する。Rustでは思ったよりも手軽に非同期処理を実装できることを実感できるはずだ。

新規プロジェクトとソースコードを用意

 並行サーバーを実装するために、新しいプロジェクトを用意する。

cargo new async-http-server

 並行サーバーの実装では、非同期プログラミングを利用可能にする「tokio」というクレートを利用する。そのためには、Cargoの設定ファイルである「Cargo.toml」の[dependencies]というセクションに次のような記述を追加する必要がある。

[dependencies]
tokio = { version = "1.20.1", features = ["full"] }

 tokioでは、非同期プログラミングを可能にするいくつかの実装と、非同期処理を動かすために必要な実行基盤(ランタイム)を提供している。Rustは、標準では非同期ライブラリーやランタイムを搭載していない。そのため、Rustで非同期プログラミングを行う際は、tokioなどのクレートを利用する。

 tokioは、Cargo.tomlに記述する「features」で利用する機能を細かく切り替えられる。今回はすべての機能を利用できる「full」を指定した。設定の手間は省けるが、機能を絞った場合と比較して依存関係が増えるため、コンパイル時間が伸びたりバイナリーサイズが増えたりするデメリットもある。

 反復サーバーのときと同様に、公開するWebページ「index.html」も用意しよう。このファイルをasync-http-serverディレクトリーのルートに置く。

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8" />
    <title>サンプルページ</title>
  </head>

  <body>
    <h1>ご挨拶</h1>
    <p>こんにちは、Rust!</p>
  </body>
</html>

 並行サーバーのソースコードは次のようになる。

use tokio::{
    fs::read_to_string,
    io::{AsyncReadExt, AsyncWriteExt},
    net::{TcpListener, TcpStream},
};

async fn start() -> std::io::Result<()> {
    let listener = TcpListener::bind("0.0.0.0:9999").await?;
    while let Ok((stream, _)) = listener.accept().await {
        tokio::spawn(handler(stream));
    }
    Ok(())
}

async fn handler(mut stream: TcpStream) -> std::io::Result<()> {
    let mut buffer = [0; 1024];
    stream.read(&mut buffer).await?;

    let get = b"GET / HTTP/1.1\r\n";

    let response = if buffer.starts_with(get) {
        let status_line = "HTTP/1.1 200 OK";
        let html = read_to_string("index.html").await?;
        let http_header = format!(
            "Content-Length: {}\r\nContent-Type: text/html;charset=UTF-8",
            html.len()
        );
        format!("{}\r\n{}\r\n\r\n{}", status_line, http_header, html)
    } else {
        "HTTP/1.1 404 NotFound\r\n\r\n".to_string()
    };
    stream.write_all(response.as_bytes()).await?;
    stream.flush().await
}

#[tokio::main]
async fn main() -> std::io::Result<()> {
    start().await
}