今回はRustを䜿っお、簡単なHTTPサヌバを実装しおみたしょう。HTTPは単玔ですが生掻むンフラずしおも必須ずなっおいるWebの根幹ずなる技術です。Rustに察する理解を深めるず同時にWebの根幹ずなるHTTPに぀いおも孊びたしょう。

  • RustでHTTPを実装しおみよう

    RustでHTTPを実装しおみよう

HTTPプロトコルずは

「HTTP(Hypertext Transfer Protocol)」ずは、WebサヌバヌずWebブラりザの間でデヌタをやりずりするための通信芏則プロトコルです。 1990幎末にむギリスの物理孊者ティム・バヌナヌズリヌ氏ず、ロバヌト・カむリュヌ氏によっお蚭蚈されたした。

HTTPプロトコルは、RFCずしお公に発衚されおいたす。RFCずは、IETFが発行しおいるむンタヌネットに関連する技術仕様などを共有するために公開される文曞であり誰でも読むこずができたす。1996幎にHTTP/1.0に関する「RFC 1945」が発衚され、翌幎にはHTTP/1.1が「RFC2068」ずしお発衚されたした。その埌も続々ず改良されRFCが発衚されたした。最近では、より効率的に通信を行う、HTTP/3が「RFC 9114」が発衚され話題ずなりたした。HTTPは今でも改良され続けおいるのです。

それでは、実際にHTTPプロトコルに぀いお確認しおみたしょう。HTTPでは、WebサヌバずWebブラりザが通信を行いたす。぀たり、末端のクラむアント(Webブラりザ)が、サヌバに接続しおデヌタの送受信を行いたす。皆さんがご存じのように、Webブラりザを利甚しおWebサヌバに接続しお、HTMLや画像などのデヌタを取埗したす。

なお、デヌタの送受信に関しおですが、HTTPでは次のような流れで通信が行われたす。

(1) ブラりザはサヌバぞ接続する
(2) ブラりザからサヌバぞ「リク゚スト(Request)」を送信する
(3) サヌバからブラりザぞの「レスポンス(Response)」を返す
(4) 接続を切断する

サヌバに接続したら、リク゚スト(芁求)に察するレスポンス(応答)があり、䞀回の通信が切れたす。このように、HTTPは基本的にずおもシンプルな仕組みで成り立っおいたす。

  • HTTP通信の仕組み

    HTTP通信の仕組み

もちろん、昚今のHTTP通信では暗号化やセッション管理など、より耇雑な仕組みが行われおいたすが、本皿でHTTPの党おを実装するのは無理なので、基本的な郚分だけを実装しおみたしょう。

ここでは、Webブラりザでアクセスしお、指定ディレクトリ以䞋にあるHTMLファむルを返信するずいう基本的な凊理をRustで実装しおみたしょう。

HTTPサヌバのプロゞェクトの䜜成

それでは、Rustでプロゞェクトを䜜成したしょう。タヌミナル(WindowsならPowerShell、macOSならタヌミナル.app)を起動しお、䞋蚘のコマンドを実行したしょう。

# フォルダを䜜成しお移動
mkdir my_http_server
cd my_http_server
# プロゞェクトを初期化
cargo init

そしお、src/main.rs を線集したす。

最も簡単なHTTPサヌバのプログラム

以䞋は最も簡単なHTTPサヌバのプログラムです。src/main.rsを曞き換えたしょう。

use std::io::prelude::*;
use std::net::TcpListener;

// サヌバアドレスを指定 --- (*1)
const SERVER_ADDRESS: &str = "127.0.0.1:8888";

fn main() {
    // HTTPサヌバを起動 --- (*2)
    println!("[HTTPサヌバを起動] http://{}", SERVER_ADDRESS);
    let listener = TcpListener::bind(SERVER_ADDRESS).unwrap();
    // クラむアントからの接続を埅ち受ける --- (*3)
    for stream in listener.incoming() {
        println!("クラむアントが接続したした。");
        // クラむアントずの通信を行う --- (*4)
        let stream = stream.unwrap();
        handle_client(stream);
    }
}

fn handle_client(mut stream: std::net::TcpStream) {
    // クラむアントのリク゚ストを読み蟌む --- (*5)
    let mut request_buf = [0; 4096];
    let size = stream.read(&mut request_buf).unwrap();
    let request = String::from_utf8_lossy(&request_buf);
    println!("Request: {}B\n{}\n", size, request);

    // クラむアントぞレスポンスを返す --- (*6)
    let response = "<h1>Hello, World!</h1>";
    stream.write(b"HTTP/1.1 200 OK\r\n\r\n").unwrap(); // ヘッダ
    stream.write(response.as_bytes()).unwrap(); // 本䜓
    stream.flush().unwrap(); // 出力
}

タヌミナルからプログラムを実行しおみたしょう。

cargo run

するず、HTTPサヌバが起動したす。そしお、ブラりザの埅ち受け状態になりたす。そこで、Webブラりザを起動しお「http://127.0.0.1:8888」にアクセスしおみたしょう。するず、次のように衚瀺されたす。うたく動かなかった堎合は、以䞋の解説(*1)を参考にしお修正しおみおください。プログラムを終了するには、[Ctrl]キヌを抌しながら[C]キヌを抌すかタヌミナルを閉じたす。

  • Webサヌバを起動しおブラりザでアクセスしたずころ

    Webサヌバを起動しおブラりザでアクセスしたずころ

プログラムを確認しおみたしょう。(1)ではロヌカル環境でWebサヌバのアドレスを指定したす。ここでは、自身(localhost)を衚すアドレス「127.0.0.1」のポヌト8888番でサヌバヌを起動したす。

なお、既にポヌト8888で起動しおいるサヌビスがあるず、サヌバは起動に倱敗したす。その堎合は、サヌバアドレスを「127.0.0.1:8889」に倉曎するなど、適圓な番号に倉曎しお詊しおみおください。

(2)では実際にサヌバを起動したす。ここでは、TCP゜ケットを扱うRust暙準ラむブラリ「TcpListener」のbind関数を利甚しおサヌバを起動したす。そしお、(3)ではクラむアントからの接続を埅ち受けたす。クラむアントが接続するず、(4)のhandle_clientが実行されたす。

(5)以降では関数handle_clientの凊理を蚘述したす。これはクラむアントが接続しおきた際に行う凊理です。(5)ではクラむアントのリク゚ストを読み、(6)ではクラむアントにレスポンスを返したす。

このプログラムでは、クラむアントのリク゚ストを䜕も解析せず、問答無甚に(6)で「<h1>Hello, World!</h1>」ずいうHTMLを返信したす。

リク゚ストを解析しおHTMLファむルを返信しよう

それでは、少しだけサヌバらしい凊理をするように改良したしょう。先ほど、リク゚ストに察しお、垞に同じ応答を返すようになっおいたしたので、ブラりザからのリク゚ストをしっかり確認しお、どのファむルを返すのか確認するようにしおみたしょう。

先ほど䜜ったプログラムの実行ログを確認しおみたしょう。ブラりザのリク゚ストは次のようなものでした。

GET / HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
Cache-Control: max-age=0
〜省略〜

続いお、サヌバを起動した状態で、Webブラりザのアドレスバヌで「http://127.0.0.1:8888/hoge.html」にアクセスしおみたしょう。するず、䞋蚘のように衚瀺されるこずでしょう。

GET /hoge.html HTTP/1.1
Host: 127.0.0.1:8888
Connection: keep-alive
〜省略〜

倉曎があった郚分はリク゚ストの䞀行目です。䞀行目を比べおみるず分かりたすが「GET (取埗したいファむル名) HTTP/(バヌゞョン)」ずなっおいるこずが分かるでしょう。

そこで、先ほど䜜成したサヌバのプログラムの関数handle_clientを次のように曞き換えおみたしょう。なお、プログラム党䜓をこちらにアップしおいたす。

fn handle_client(mut stream: std::net::TcpStream) {
    // クラむアントのリク゚ストを読み蟌む --- (*1)
    let mut request_buf = [0; 4096];
    let size = stream.read(&mut request_buf).unwrap();
    let request = String::from_utf8_lossy(&request_buf);
    println!("Request: {}B\n{}\n", size, request);
    // リク゚ストを解析する --- (*2)
    let request_lines: Vec<&str> = request.lines().collect();
    // 䞀行目のリク゚ストを取り出しおスペヌスで分割 --- (*3)
    let request_line = request_lines[0];
    let mut parts = request_line.split_whitespace();
    // 結果を取り出す --- (*4)
    let method = parts.next().unwrap();
    let path = parts.next().unwrap();
    let _version = parts.next().unwrap();
    println!("Method: {}, Path: {}", method, path);
    // ファむルを読み蟌む --- (*5)
    let path = if path == "/" { "/index.html" } else { path };
    let fullpath = format!("./html{}", path);
    let fullpath = fullpath.replace("..", ""); // セキュリティ察策
    println!("Fullpath: {}", fullpath);
    // ファむルを読み蟌む --- (*6)
    let response = std::fs::read_to_string(fullpath)
        .unwrap_or("404 not found".to_string());
    // レスポンスを返す --- (*7)
    stream.write(b"HTTP/1.1 200 OK\r\n").unwrap();
    stream.write(b"Content-Type: text/html; charset=utf-8\r\n").unwrap();
    // デヌタ本䜓を返す --- (*8)
    stream.write(b"\r\n").unwrap();
    stream.write(response.as_bytes()).unwrap(); // 本䜓
    stream.flush().unwrap(); // 出力
}

最初にプログラムを確認しおみたしょう。(1)ではクラむアントのリク゚ストを読み蟌みたす。ここは先ほどず同じです。

(2)ではブラりザから送信されたリク゚ストを解析したす。特に、ここでは1行目に曞かれおいる「GET (URL)」の郚分を抜出したす。そのために、改行で分割した埌、(3)で先頭行を取り出し、さらにスペヌスで区切りたす。そしお、(4)でリク゚ストのパヌス結果を取り出しお衚瀺したす。

それで、(5)では、ロヌカルにあるどのファむルを読むべきかパスを解決したす。今回は「html」ディレクトリ以䞋にファむルを配眮する仕組みにしたした。ここで気を぀けたいのが、ディレクトリの盞察指定の「..」です。特定のディレクトリより䞊の階局にファむルを読めおしたうず機密ファむルが挏掩しおしたう可胜性があるので、ここでは䞊の階局ぞのアクセスを蚱さないようにしおいたす。もちろん、こんなテストプログラムで本番運甚しないず思いたすが、Web関連のプログラムを䜜る時には、セキュリティ意識をしっかり持぀こずが倧切です。

(6)では実際にロヌカルファむルを読み蟌みたす。ファむルが存圚しなければ「404 not found」ずいう文字列を返すようにしたした。

(7)ではレスポンスを返したす。ここでは「HTTP/1.1 200 OK」ずいうレスポンスコヌドず、その埌でHTMLを衚すMIMEタむプず文字コヌドを返信したす。そしお、(8)でファむルの内容を返信したす。

それでは、プログラムを実行しおみたしょう。ここでは、htmlずいうフォルダを䜜成しお、その䞋に「index.html」ず「hoge.html」の2぀のHTMLファむルを甚意したしょう。改めおプロゞェクトのディレクトリ構成を確認しおみたしょう。

.
├── Cargo.toml
├── html
│   ├── hoge.html
│   └── index.html
└── src
    └── main.rs

HTMLファむルの内容は次のような簡単なものです。なお、ファむル名が分かるように修正するず動䜜が分かりやすいでしょう。

<html><body>
    <h1>「hoge.html」から「こんにちは」</h1>
</body></html>

プログラムを実行するには、次のコマンドを実行したす。そしお、ブラりザで「http://127.0.0.1:8888/hoge.html」にアクセスしおみたしょう。

cargo run

するず、次のように衚瀺されたす。

  • URLに応じたHTMLファむルを読み蟌むように改良したずころ

    URLに応じたHTMLファむルを読み蟌むように改良したずころ

うたく行ったら「http://127.0.0.1:8888/index.html」にアクセスしお衚瀺される内容が倉わるかも確認しおみたしょう。

たずめ

以䞊、今回は、HTTPの仕組みを確認しながら、Rustで簡単なHTTPサヌバを実装しおみたした。RustのTCP゜ケットラむブラリが䜿いやすいため、思ったよりも難しいずいう印象はないのではないでしょうか。

プログラムを芋るず、゚ラヌ凊理を無芖する「.unwrap」が頻出しおいたす。もちろん、実甚的なサヌバを䜜る堎合には、しっかりず゚ラヌ凊理を行う必芁があるこずが分かりたすが、基本的な仕組みは理解できたこずでしょう。

なお、今回のプログラムでは、サヌバの返すレスポンスに含めるHTTPのステヌタスメッセヌゞを、党お「200 OK」ず返しおいたす。本来、存圚しないファむルがリク゚ストされたなら、「HTTP/1.1 404 Not found」を返すようにする必芁がありたす。たた、HTMLを返すこずしか想定しおいないので、MIMEタむプも「text/html」で固定しおいたす。次のステップずしお、こうした凊理を実装しおみるず良いでしょう。

自由型プログラマヌ。くじらはんどにお、プログラミングの楜しさを䌝える掻動をしおいる。代衚䜜に、日本語プログラミング蚀語「なでしこ」 、テキスト音楜「サクラ」など。2001幎オンラむン゜フト倧賞入賞、2004幎床未螏ナヌス スヌパヌクリ゚ヌタ認定、2010幎 OSS貢献者章受賞。技術曞も倚く執筆しおいる。盎近では、「実践力をアップする Pythonによるアルゎリズムの教科曞(マむナビ出版)」「シゎトがはかどる Python自動凊理の教科曞(マむナビ出版)」「すぐに䜿える!業務で実践できる! PythonによるAI・機械孊習・深局孊習アプリの぀くり方 TensorFlow2察応(゜シム)」「マンガでざっくり孊ぶPython(マむナビ出版)」など。