Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
Help us understand the problem. What is going on with this article?

Rustで簡易HTTPサーバーを作成してみる

More than 1 year has passed since last update.

Qiita初投稿がAdventCalendarで大変恐縮ですがよろしくお願いします。
最近HTTPサーバーの仕組みについて勉強したいと思っておりC言語で実装してみています。
また、Rustにも興味がありましたので、この機会にRustで簡易的なHTTPサーバーを作成してみましたのでそのご紹介をさせていただきます。

実行環境

ArchLinux
rustc 1.22.1 (05e2e1c41 2017-11-22)

実装

まずmainの実装は以下のようになりました。

main()
fn main() {
    let listener = TcpListener::bind("127.0.0.1:8080").unwrap();

    for stream in listener.incoming() {
        match stream {
            Ok(stream) => {
                thread::spawn(move || {
                    handle_client(stream)
                });
            }
            Err(_) => { panic!("connection failed") }
        };

    }
}

TCPの接続周りはstd::netモジュールを使用しました。TcpListener.bindでipアドレスとポート番号を割り当てたTcpListenerを作成します。
incoming()はloop~acceptと同様の動きをしているらしいのでそれによって、接続をループで待ち受けます。
接続が来た場合、スレッドを作成し、handle_clientに処理を任せます。

handle_clientの処理は以下のようになりました。(2017/12/04 ご指摘いただいた部分修正させていただいております。@ubnt_intrepidさん、ありがとうございます)

handle_client()
fn handle_client(stream: TcpStream) {
    let mut stream = io::BufReader::new(stream);

    let mut first_line = String::new();
    if let Err(err) = stream.read_line(&mut first_line) {
        panic!("error during receive a line: {}", err);
    }

    let mut params = first_line.split_whitespace();
    let method = params.next();
    let path = params.next();
    match (method, path) {
        (Some("GET"), Some(file_path)) => {
            get_operation(file_path, stream.get_mut());
        }
        _ => panic!("failed to parse"),
    }
}

ここではstreamをBufReaderでラップしたものを使用して、送られてきた情報を読み取り分割していっています。
BufReaderのread_lineを使用して一行目だけを取得し split_whitespace() でスペースごとに分割しています。
接続側でcurl localhost:8080 -v を実行したとすると

> GET /test.html HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.56.1
> Accept: */*

このリクエストの情報のうち GET /test.html HTTP/1.1 が空白区切りでparamsに入ったことになります。
先頭の値(メソッド)がGETの場合get_operationを実行するようになっています。(2017/12/04 こちらもご指摘いただいた部分修正させていただいております。@ubnt_intrepidさん、ありがとうございます)

get_operation
fn get_operation(file_name: &str, stream: &mut TcpStream) {
    let path = PathBuf::from(format!("./www{}", file_name));
    let mut file = match File::open(&path) {
        Err(why) => {
            panic!(
                "couldn't open {}: {}",
                path.display(),
                Error::description(&why)
            )
        }
        Ok(file) => file,
    };
    let len = file.metadata().map(|m| m.len()).unwrap_or(0);

    writeln!(stream, "HTTP/1.1 200 OK").unwrap();
    writeln!(stream, "Content-Type: text/html; charset=UTF-8").unwrap();
    writeln!(stream, "Content-Length: {}", len).unwrap();
    writeln!(stream).unwrap();

    io::copy(&mut file, stream).unwrap();
}

get_operationでは、streamを使ってresponseのヘッダー行を固定で出力してしまっています。(どのバージョンのHTTPリクエストが来ても1.1で返してしまっています)
あとはパス指定されたファイルをreadで読み取り、streamに書き込んでいっているだけとなっています。

まとめ

今回のHTTPサーバーはGetしか実装していませんし、レスポンスも固定ですが、簡易サーバーなのでご了承ください。
コードの全文等は以下においてありますので、ご興味がある方はご確認ください。
何か間違いや改善点などございましたらご指摘いただければ幸いです。

https://github.com/worldcreate/simple-httpd

参考

sogrnwil
zyyx
新しい世界の核心に触れるために。我々ジークスは、技術と感性で、テクノロジーがもたらす新しい世界を作り続けます。
https://www.zyyx.jp/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away