Edited at

RustでHttpServer

More than 1 year has passed since last update.


概要

rosettacodeにあったコードをほとんどそのまま書くので、自分で作ったというのではなくrosettacodeにあるコードが

どうなっているかということをゆっくり見ていく。また、Rustについては初心者のため、もっと理解したらその都度更新していく。

rosettacodeとは何なのかというと以下のとおり。私は知らなかった。


「Rosetta Code」は、「いろいろなタスク(例題)を、様々なプログラミング言語で書いたらどうなるか」を比較できるプログラミング言語の名句集サイトです。

https://www.softantenna.com/wp/webservice/rosetta-code/



やってみる(というか見てみる)


全体のコード

まず全体のコードが以下。これを分解してみていく。

use std::net::{Shutdown, TcpListener};

use std::thread;
use std::io::Write;

const RESPONSE: &'static [u8] = b"HTTP/1.1 200 OK\r
Content-Type: text/html; charset=UTF-8
\r\n\r
<!DOCTYPE html><html><head><title>Bye-bye baby bye-bye</title>
<style>body { background-color: #111 }
h1 { font-size:4cm; text-align: center; color: black;
text-shadow: 0 0 2mm red}</style></head>
<body><h1>Goodbye, world!</h1></body></html>
\r";

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

for stream in listener.incoming() {
thread::spawn(move || {
let mut stream = stream.unwrap();
match stream.write(RESPONSE) {
Ok(_) => println!("Response sent!"),
Err(e) => println!("Failed sending response: {}!", e),
}
stream.shutdown(Shutdown::Write).unwrap();
});
}
}


宣言等

use std::net::{Shutdown, TcpListener};

use std::thread;
use std::io::Write;

const RESPONSE: &'static [u8] = b"HTTP/1.1 200 OK\r
Content-Type: text/html; charset=UTF-8\r\n\r
<!DOCTYPE html><html><head><title>Bye-bye baby bye-bye</title>
<style>body { background-color: #111 }
h1 { font-size:4cm; text-align: center; color: black;
text-shadow: 0 0 2mm red}</style></head>
<body><h1>Goodbye, world!</h1></body></html>\r";

しょっぱなではあるが、ここはこういうものなのかな、で済ませる。

定数Responseの型が&'static [u8]として宣言されている理由は、この後見ていくがResponseを返す際にwrite()を使用する。このwrite()が引数としてu8のデータを期待するため&'static [u8]となっている。

'staticについてはライフタイムの記事を読んでみて、ライフサイクル全体で有効にしておきたい変数を定義する際に必要な記述なんだろうと解釈した。


TCPリスナーの作成

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

bind()は引数に指定されたアドレスをもとに新しいTcpListerを作成する。

また、Result<TcpListener>型でデータが返ってくるためunwrap()をして内包されるTcpListerを取得する。

今回はbind()のアドレスは1つしか指定していないが、

以下のように指定することでポート80への接続で失敗した際にポート443に向いたTcpListerを作成することもできる。


複数指定

let addrs = [

SocketAddr::from(([127, 0, 0, 1], 80)),
SocketAddr::from(([127, 0, 0, 1], 443)),
];
let listener = TcpListener::bind(&addrs[..]).unwrap();

参考

TcpLister - bind

unwrap()について


Listenをする部分

for stream in listener.incoming() { ... }

incoming()メソッドを利用するとIncoming構造体を取得することができる。

これは対象のTcpListerに対する接続要求を待ち続けるイテレータであるためループ処理を利用してListenをさせる。

参考

Incoming


thread作成

thread::spawn(move || { ... })

thread::spawnはスレッド生成の標準ライブラリである。スレッドを動かす際の基本的な形はthread::spawn(|| { ... })であり、渡された無名関数が別スレッドで動作する。

今回は生成したスレッドにおいて変数streamを使用したい。そのためには、所有権の問題にぶち当たってしまうためstreamのコピーの所有権を渡るようにするために、基本構文にmoveキーワードを付与する。

参考

Rustでマルチスレッドプログラミングのメモ


レスポンスをする

let mut stream = stream.unwrap();

match stream.write(RESPONSE) {
Ok(_) => println!("Response sent!"),
Err(e) => println!("Failed sending response: {}!", e),}
stream.shutdown(Shutdown::Write).unwrap();

Incomingオブジェクトをループした際に取得したstreamの型はResult<TcpStream>である。したがってunwrap()をすることでTcpStream型のデータを取得することができる。

コネクションを通じたデータのやり取りはread()もしくはwrite()を通して行わる。今回はレスポンスだけを行うのでwrite()を使用する。write()の結果が成功した場合はOkが呼ばれ失敗した場合はErrが呼ばれる。

参考

matchの使い方

また、個々の接続は閉じられる必要があるためshutdownメソッドを利用して閉じる。今回はwriteしか行っていないので引数としてShutdown::Writeを指定するが、readを行う場合はShutdown::Read、両方を使用した場合はShutdown::Bothを使用する。ところで最後にshutdownメソッドの戻り値に対してunwrap()をする理由はわからなかった。どこで使われるわけでもない値なので、必要ないと思ったが…、ご存知の方がいればアドバイスをいただきたく思います。


結果

ちゃんとブラウザで結果を表示させることができた。

また、コンソール側でもResponse sent!が表示された(なぜか3回出た…)。

(2018/5/7追記 3回出た理由はわかりませんが、2回表示される理由は以下のコメントにあります。)

もっと理解を深めて言葉を正確に使った解説をしていけるようになりたい。

Compiling hello_cargo v0.1.0 (file:///xxx/rust/hello_cargo)

Finished dev [unoptimized + debuginfo] target(s) in 0.91 secs
Running `target\debug\hello_cargo.exe`
Response sent!
Response sent!
Response sent!


参考

https://gist.github.com/mjohnsullivan/e5182707caf0a9dbdf2d