LoginSignup
38
26

More than 3 years have passed since last update.

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

Last updated at Posted at 2017-12-03

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

参考

38
26
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
38
26