Webサーバー
ブラウザーが話しかけてくれるシェルスクリプト20行。
Webサーバーはリクエストを待ってレスポンスを送るだけのプログラムです。それを証明するために、bashとnetcatと名前付きパイプだけでサーバーを作ります — 一行残らず読めます。そのあと、実際のサイトをHTTPS込みで動かすCaddyへ進みます。
Webサーバーは答えるプログラムです
ラックとクラウドのロゴを取り払えば、Webサーバーは一つのことを繰り返すプログラム一つです。誰かがつなぐのを待ち、何が欲しいかを読み、文章を返し、また待ちます。
「サーバー」という語は二つのものを指し、人はいつも混ぜて使います。データセンターに置かれた機械もサーバー、ポートで耳を澄ませるプログラムもサーバーです。ここで言うのはプログラムのほうで、それは目の前のノートパソコンでも同じように動きます。
訪問者一人、四つの歩み
- 耳を澄ます プログラムがポートを一つ占めて待ちます — 例では8084です。ポートは機械に付いた番号付きの扉なので、二つのプログラムが同じ扉を握ることはできません。
- リクエストを読む ブラウザーがつないで数行の文章を送ります。一行目に、どの方法でどのパスが欲しいかが入ります。たとえばパス / をGETしてくれ、という具合です。
- 判断する 何を送るかを決めます。ディスクからファイルを読む、コードを走らせる、データベースに問う。例はいつも同じファイルを送るだけなので20行に収まります。
- 答えて切る ステータス行といくつかのヘッダー、空行を一つ、そして中身を書いて送り、接続を切って次の訪問者のために最初へ戻ります。
あなたが名前を聞いたことのあるサーバーは — Apache・Nginx・Caddy、このサイトを動かしているNodeプロセスまで — 上の四歩をしています。違うのは、それを何千人にも同時に、安全に、HTTPSで行うという点だけです。形は同じです。
シェルスクリプト一枚で作るWebサーバー
下がbashで書いた完全なWebサーバーです。ポートを開き、リクエスト行を読み、記録を残し、index.htmlをきちんとしたHTTPレスポンスとして返します。二十行、ライブラリなし、そしてブラウザーは違いに気付きません。
重い仕事は二つのコマンドが担います。netcat(nc)がネットワークを話し、mkfifoが名前付きパイプを作って、書いた答えをncが握っている接続へ戻します。
#!/bin/bash
PORT=8084
PIPE=simple-web-server-pipe
rm -f "$PIPE"
mkfifo "$PIPE"
response() {
read -r method path _
echo "[$(date)] $method $path" >&2
index=$(cat index.html)
printf 'HTTP/1.1 200 OK\r\nContent-Type: text/html; charset=utf-8\r\nConnection: close\r\n\r\n%s' \
"$index"
}
while true; do
response < "$PIPE" | nc -l "$PORT" > "$PIPE"
done一行ずつ読む
PORT=8084耳を澄ますポートです。1024より大きい番号は管理者権限なしで使え、8084なら既に動かしているものとぶつかりにくいでしょう。PIPE=simple-web-server-pipeパイプファイルの名前です。フォルダーにはふつうのファイルのように現れますが、中に保存されるものはありません — 二つのコマンドの間をデータが通り抜けるだけです。mkfifo "$PIPE"名前付きパイプ(FIFO)を作ります。すぐ上の行が前回の残りを消しているのは、mkfifoが既にあるファイルへの上書きを拒むからです。read -r method path _リクエストの一行目を読んで、メソッド・パス・残りに分けます。その後に続くブラウザーのヘッダーは、このサーバーは読み飛ばします。echo "[$(date)] $method $path" >&2記録を一行残します。標準エラーへ向きを変えているおかげで、この文がブラウザーへ行かずターミナルにだけ現れます。index=$(cat index.html)今いるフォルダーのindex.htmlを読んで変数に入れます。サーバーをそのファイルのあるフォルダーで起動しなければならない理由がこれです。printf 'HTTP/1.1 200 OK\r\n…'レスポンスを書きます。ステータス行、中身の種類、ここで接続を終えるというヘッダー、そして空行とページ。ヘッダーと中身を分けるのがその空行で、HTTPはすべての行末にキャリッジリターンと改行を求めます。response < "$PIPE" | nc -l "$PORT" > "$PIPE"これが動く仕掛けです。ncはパイプからこちらのレスポンスを読み、ブラウザーのリクエストをそのパイプへ書き戻します。両側が互いに餌を与え合う形です。while true; do … done繰り返しです。ncは接続を一つ処理すると終わってしまうので、この行がなければサーバーは訪問者を一人受けて止まります。
リクエストが関数に届いて初めて、関数の答えがncに届きます — 輪の形です。ところがシェルのパイプラインは一方向にしか流れません。名前付きパイプがその輪を閉じます。レスポンスはパイプラインを通ってncへ、リクエストはFIFOを通って戻ってきます。二つのコマンドを双方向で会話させる、いちばん小さな方法です。
自分で動かす
スクリプトをserver.shとして保存し、隣にindex.htmlを置いて起動します。そのターミナルは開いたままにして、二つ目のターミナルからリクエストを送ってみてください。
# 1. create an index.html in the same folder
echo '<h1>Hello</h1>' > index.html
# 2. save the script above as server.sh and start it
bash server.sh
# 3. open it in a browser (or check from a second terminal)
open http://localhost:8084
curl http://localhost:8084ブラウザーを再読み込みしながら一つ目のターミナルを見てください。リクエスト一件ごとに記録が一行増えます。Control-Cで止め、パイプファイルが残っていたら消します。LinuxのncはポートのまえにさらにPオプションが要ることがあり、macOSに標準で入っている版は書いたとおりに動きます。
線の上を実際に流れるもの
接続を開いてみると、自分でも打てたはずのふつうの文字が流れています。シェルWebサーバーが成り立つ理由がこれであり、Webの不具合を当て推量ではなく読んで直せる理由もこれです。
ブラウザーが送るリクエスト
GET / HTTP/1.1
Host: localhost:8084
User-Agent: Mozilla/5.0
Accept: text/html一行目が、このサーバーが読む唯一の部分です。メソッド(GETは取ってくる、POSTは送る)、パス、そしてプロトコルの版。 Hostはどのサイトが欲しいかを伝えます。一つのサーバーが一つの住所で何百ものドメインを抱えられ、それを見分けるのがこのヘッダーです。 残りはブラウザーの自己紹介です。自分がどんなソフトか、どんな形式を読めるか、どの言語が好みか。サーバーは使ってもよいし、無視してもかまいません。
サーバーが送るレスポンス
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
Connection: close
<h1>Hello</h1>ステータス行です。プロトコルの版と、どうなったかを告げる数字。200は成功という意味です。 Content-Typeは、続くものをどう読むべきかをブラウザーに教えます。HTMLを間違った種類で送ると、ブラウザーはタグをページではなく文字のまま見せます。 charsetの部分は実際には省けません。これがないと日本語は文字化けして届きがちです。 そして空行が一つ。その後ろはすべて中身です。ヘッダーとページを分ける境目は、その空行だけです。
覚えておきたいステータスコード
| コード | 意味 |
|---|---|
| 200 OK | 成功です。求めたものがレスポンスに入っています。200番台はどれもこの種の成功を意味します。 |
| 301 Moved Permanently | 住所が恒久的に移りました。新しい住所はLocationヘッダーにあり、ブラウザーが自動でついていきます。サイトがhttpの訪問者をhttpsへ送るときの手口です。 |
| 404 Not Found | サーバーは元気ですが、そのパスがありません。400番台は問題がリクエスト側にあるという意味です。 |
| 500 Internal Server Error | まっとうなリクエストを処理する途中でサーバーが倒れました。500番台は直すべき人があなただという意味で、詳しい話はサーバーの記録にあります。 |
このサーバーにできないこと
この例は正直な教材であり、ひどいサービスです。どこが足りないかを正確に知ることが、本物のサーバーが代わりに解いてくれる問題を知ることです。
一度に一人だけ。ncは接続を一つしか扱わないので、それがふさがっている間ほかの全員が待ちます。 パスを無視します。何を求めてもindex.htmlが返るだけ — 道案内もなく、二枚目のページもありません。 すべてHTMLとして送ります。画像もスタイルシートもダウンロードもそれぞれの種類が要るのに、このサーバーは一種類しか知りません。 HTTPSがありません。すべてが裸で流れるので、途中のどのネットワークでも読めますし書き換えられます。 安全装置が一切ありません。リクエストの大きさを測らず、時間切れもなく、うかつに広げれば他人が決めたファイルパスをそのまま読んでしまいます。
自分の機械で動かしてHTTPを目で見ることに使い、他人が訪ねてくるものには本物のサーバーを使います。次の節がその本物で、始めるのはこれより大して難しくありません。
Caddy — 読めるまま残った本物のWebサーバー
Caddyは実行ファイル一つでできた現代的なWebサーバーです。シェルの例のすぐあとに学ぶ価値があるのは、読める数行で動くという感覚をそのままに、シェル版にできなかったことをすべて片づけるからです。
いちばんの売りは自動HTTPSです。本物のドメイン名を与えるだけで証明書を取り、入れ、期限が来る前に更新し、httpの訪問者をhttpsへ回します。ほかのサーバーでは数か月ごとに繰り返す雑用です。
インストール
# macOS
brew install caddy
# Ubuntu / Debian
sudo apt install -y debian-keyring debian-archive-keyring apt-transport-https
sudo apt update && sudo apt install caddy
# Docker alone is enough
docker run -p 8080:80 -v "$PWD":/usr/share/caddy caddyコマンド一行でフォルダーを差し出す
設定ファイルは要りません。index.htmlのあるフォルダーへ行って一行打つだけ — 同じネットワークの誰かにページを見せるいちばん速い方法です。
# serve this folder's index.html on port 8080
caddy file-server --listen :8080
# add a directory listing for folders without an index.html
caddy file-server --listen :8080 --browseCaddyfile
長く置くものならCaddyfileを書きます。ただの文字ファイルで、サイト一つが四行で終わります。
example.com {
root * /var/www/html
file_server
encode gzip zstd
}example.com {サイトの住所です。ここに本物のドメインを書けばCaddyがHTTPS証明書を自分で用意し、localhostと書けばローカル作業用にふつうのhttpを出します。root * /var/www/htmlファイルがディスクのどこにあるかです。星印はこのサイトのすべてのパスに当てはめるという意味です。file_serverそのファイルを実際に差し出せ、という行です。この行がないとCaddyはリクエストに何も返しません。encode gzip zstd送る前にレスポンスを圧縮します。一語ですが、遅い回線ではページが目に見えて速く届きます。
CaddyはLet's Encryptに無料の証明書を求めます。そのためにはドメインのDNSが自分の機械を指し、80番と443番のポートに外から届く必要があります。前にいる別のプロキシがそのポートを受けていると確認に失敗し、証明書は届きません — 最初のCaddy設定で鍵が緑にならない、いちばんよくある理由です。
リバースプロキシ — リクエストを渡す
たいていのサイトはディスク上のファイルではなく、どこかのローカルポートで耳を澄ませているアプリケーションです。リバースプロキシは外からのリクエストを受けてそのアプリへ渡し、答えを返すサーバーです。一行で済みます。
getpes.com {
reverse_proxy pes-web:3000
}
dl.getpes.com {
root * /srv/files
file_server browse
}一台の機械が複数のサイトを抱える方法もこれです。443番ポートは一つのプログラムしか持てないのでCaddyがそれを握り、訪問者がどのドメインを求めたかを読んで、それぞれ別のアプリへ回します。
必要なコマンド
| コマンド | 働き |
|---|---|
| caddy run | 前面で動かし、記録をターミナルに出します。設定をまだ整えている間はこちらが欲しいはずです。 |
| caddy start | 背後へ回してプロンプトを返します。ちょっと見せるには十分ですが、きちんとしたサーバーはシステムサービスとして動かします。 |
| caddy reload | 接続を一つも切らずに、変えた設定を読み直します。Caddyfileを直したからといって再起動は要りません。 |
| caddy fmt --overwrite | ファイルの書式をその場で整えます。字下げは見た目以上に大事で、このコマンドがその争いを終わらせます。 |
| caddy validate | 何も起動せずに設定の誤りを調べます。動いているサーバーをreloadする前に、まずこれを回します。 |
PESのサーバーではCaddy一つが80番と443番を握り、ドメイン名で道を分けます。本サイトはアプリケーションのコンテナへ、ダウンロード用のサブドメインはディスクから直接。新しいサイトを足す作業は、上のようなファイルを一つ置いてreloadするだけです — 再起動もなく、ポートを奪い合う二つ目のプログラムもありません。
自分で記録する欄です。ここで採点はしません。