Bash で Web サーバ
〜 Practice of scripting language Bash (2) 〜
2024-11-11 作成 福島
TOP > shell > bash-www
[ TIPS | TOYS | OTAKU | LINK | MOVIE | CGI | AvTitle | ConfuTerm | HIST | AnSt | Asob | Shell ]

学習目的で活用してください。(商用がダメとかではなく、実運用に耐えられないはず)
実践的な Web サーバを構築するなら、Python だと Django とか Flask、高効率なら Apache、Nginx とかがお勧めです。

nc コマンド (NetCat) では、いくつかのオプションスイッチがあり、そのうち -l (--listen) を使用すると、簡易 Web サーバとして動作することができます。
また、-e (--exec) を使用すると、リクエストされた文字列をすべて、指定したプログラムに送り込むことができます。

ここでは、この nc コマンドに -l, -e を指定することにより、Bash のスクリプトでリクエスト文字列を処理し、結果を返します。


0. 使用するコマンドの所在を確認

$ which bash nc curl cut sed touch
/usr/bin/bash
/usr/bin/nc
/usr/bin/curl
/usr/bin/cut
/usr/bin/sed
/usr/bin/touch  
which コマンドは、実行可能パス*1のどこにコマンドが存在するかを調査するので、
このように表示されるということは、コマンド名だけで使用できることを表している。
(「/usr/bin/nc」のようにフルパス指定することなく「nc」をタイプすれば実行できる)

ここでは可読性を重視しているため、あえてフルパスを避けるように記述しているが、
保守性を考慮するならフルパスで記述すること。
(ちなみに which コマンドのフルパスは多くの場合 /usr/bin/which となっている)

*1「パス」(PATH) とは「経路」「道筋」のことで、Unix (Linux, Solaris) においては、コマンドやファイルの格納位置やエイリアス (別名) を意味します。


1. 基本形

ファイル名: www-0.sh
#!/usr/bin/bash
# Usage:
#   nc 127.0.0.1 8080 -l -e "/usr/bin/bash ./www-0.sh"

# とりあえず HTTP のレスポンスヘッダを返す
echo -e "HTTP/1.0 200 OK\n"


# 静的コンテンツ (いつものやつ) を返す echo "Hello, Bash"
  単体で実行「bash ./www-0.sh」すると結果がこうなる。
HTTP/1.0 200 OK  

Hello, Bash
 最初の 2 行はレスポンスヘッダなので curl では表示されない。

 ⇒ 
 
 
 
 
 
 
echo は Bash の内部コマンド。標準出力に文字列を書き出す。
オプション -e を付けると、文字列内にエスケープシーケンス (例: \n は改行の意味) を記述できる。
echo は自動的に改行を出力するので、ここでは "…\n" として 2 つの改行を出力している。

実行 (Web サーバ側)
$ nc 127.0.0.1 8080 -l -e "/usr/bin/bash ./www-0.sh"

*2nc コマンドの -e オプションは、なぜか「実行可能パス」を参照しないので /usr/bin/bash の様にフルパスで指定する必要がある。
実行 (Web クライアント側)
$ curl http://127.0.0.1:8080/
Hello, Bash
上記を一度に実行する場合はこうする。(上級者向け)
$ ( nc 127.0.0.1 8080 -l -e "/usr/bin/bash ./www-0.sh" & ) ; curl http://127.0.0.1:8080/
Hello, Bash
使用コマンドの説明 (1)*3
概念図
Web クライアント 通信 Web サーバ (ポート 8080)
curl 
リクエスト

レスポンス
 nc  → 標準入力
 Bash スクリプト 
 ← 標準出力

nc コマンド (エヌシー、ネットキャット)
nc HH NN としてホスト (IP アドレスまたはホスト名) HH と、ポート番号 NN を指定する。

ポート番号は 0 ~ 65535 を使うことができる。
このうち 0 ~ 1023 はシステムポートと呼び、管理者だけが使える。
一般ユーザは 1024 番以上のポート番号を指定しなければならない。(システムポートを使うとエラーになる)

オプションスイッチ「-l」は TCP 通信の接続を待ち受ける意味。
nc は、入力トリガ・出力トリガどちらにも使用できるが、ここでは入力トリガ (待ち受け: サーバー) として使用し、
IP アドレス 127.0.0.1 の 8080 番ポートで接続を待ち受けている。
「echo -n -e 'GET / HTTP/1.0\r\n\r\n' | nc www.example.jp 80」とすればクライアントになることも可能。

オプションスイッチ「-e FF」は接続から受け入れた文字列をプログラム FF に渡すという意味。
ここではプログラム bash と引数 www-0.sh を指定している。
(www-0.sh を Bash のスクリプトとして起動している)

プログラム FF では、nc から渡された文字列を標準入力から読み込むことができ、
その処理結果として標準出力へ書き出された文字列は nc に戻される。
curl コマンド (カール)
curl は主に http および https プロトコルのクライアントとして使用する。(その他のプロトコルはこちらを参照)

かなり限定された機能を持つ Web ブラウザ。
URL を引数にして実行すると、Web サーバからの結果を取得することができる。
このとき、レスポンスヘッダ (「HTTP/1.0 200 OK」等) は表示されない。
*3ここではすべての説明をしていません。気になる向きは (英語だけど) man コマンドを実行してください (例: man curl)。JM_Project は見えなくなりました


2. リクエスト文字列を処理してみる (GET リクエスト)

ファイル名: www-1.sh
#!/usr/bin/bash
# Usage:
#   nc 127.0.0.1 8080 -l -e "/usr/bin/bash ./www-1.sh"

# とりあえず HTTP のレスポンスヘッダを返す
echo -e "HTTP/1.0 200 OK\n"


# 標準入力からリクエストヘッダをすべて読み込む*4 req_get=() # GET リクエストを格納する配列 while read line do # 改行を削除する*5 line=`echo $line | sed 's/[\r\n]\+//g'` # 空行が来たらリクエストヘッダの終了 if [ "$line" == "" ]; then break; fi # GET リクエストを蓄積する*6 (実際はひとつだけ) if [[ $line =~ ^GET ]]; then req_get+=("$line") fi done
echo "<html>" echo "<body>" # GET リクエスト処理 # 本当は for req in ${req_get[@]} と記述したいが、 # GET リクエストには ' ' が含まれるのでできない。 idx="${!req_get[@]}" for i in $idx ; do # GET リクエストを 1 行取り出す line="${req_get[$i]}" # "GET /a=1 HTTP/1.1" を想定 line=`echo $line | sed 's/^GET\s\///g'` # "GET /" を取り除く dir=`echo $line | cut -d ' ' -f 1` # a=1 の部分を取り出す prt=`echo $line | cut -d ' ' -f 2` # HTTP/1.1 の部分を取り出す if [[ $dir =~ '=' ]]; then # a=1 を処理する name=`echo $dir | cut -d '=' -f 1` # 左辺を取り出す value=`echo $dir | cut -d '=' -f 2` # 右辺を取り出す # URL の引数部を出力する*7 echo -n "Name: $name" echo -n "<br>" echo -n "Value: $value" echo -n "<br>" echo fi done
# 永続性処理*8(パーシステンスともいう) source ./exLock.sh LOCKPATH=./data/lock.LK # 排他ロック処理 (開始) ExLock_lock $LOCKPATH ; ISLOCK=$? data="" if [ $ISLOCK -eq 1 ]; then # データファイルの数値をインクリメントする DATAFILE=./data/data.txt touch $DATAFILE # 次行の cat でエラーにならないよう、空ファイルを用意する # ファイルから数値を取り出し、+1 して変数に格納する data=$(($(cat $DATAFILE) + 1)) # 変数の値をファイルに書き出す echo $data > $DATAFILE fi # 排他ロック処理 (終了) ExLock_unlock $LOCKPATH $ISLOCK echo -n "<h1>" echo -n "Sequence number is '$data'." echo -n "</h1>" echo
echo "</body>" echo "</html>"
*4入力待ちなので、単体で実行すると停止したように見えます。
*7echo コマンドは通常、出力の最後に改行を付加するが -n は、この改行を抑止するオプション。スクリプトの行を削除したときの影響を最小限にしている。
*8排他ロック関数はこちらを使っています。exLock.sh は www-1.sh と同じディレクトリに設置すること。
また、高負荷が予測される場合はロックファイルを RAM 領域に作成すること。(テストで 10~20 回実行する程度なら問題ない)

実行 (Web サーバ側)
$ mkdir ./data/      # ← 予めデータ格納用ディレクトリを作成しておく
$ nc 127.0.0.1 8080 -l -e "/usr/bin/bash ./www-1.sh"
実行 (Web クライアント側 1)
$ curl http://127.0.0.1:8080/
<html>
<body>
<h1>Sequence number is '1'.</h1>  
</body>
</html>
実行 (Web クライアント側 2)
$ curl http://127.0.0.1:8080/v=10
<html>
<body>
Name: v<br>Value: 10<br>
<h1>Sequence number is '2'.</h1>  
</body>
</html>
使用コマンドの説明 (2)*9
` (バッククォート)
文字列をコマンドとみなし、実行結果を返す。
シングルクォート (') と見間違いやすいが、Bash では別の役割を持つ。

line=`echo $line | sed 's/[\r\n]\+//g'`*5では、
変数 $line の中身を sed で加工し、その結果を変数 line に格納している。
line$line は同じ変数なので、変数 line の中身を加工している。
[[ (ダブルブラケット、にじゅうかくかっこ)
Bash の if 構文では、条件式の指定に角括弧 [ (test コマンドのエイリアス) を使うことが慣例となっているが、
[[ は [ の拡張で、条件式に正規表現の比較 (=~) を記述できる。

ここでは、[[ $line =~ ^GET ]]*6 として、
変数 $line に格納されている文字列の先頭に GET があるかどうかをチェックしている。
「^」は先頭を指定する正規表現の文字。
cut コマンド (カット)
文字列を分割し、指定位置の単語を出力する。

-d SSSS で分割する文字を指定する。
ここでは、-d ' ' として空白文字で分割している。

-f NNNN で分割したあとの位置を指定する。
ここでは、-f 1 として最初の文字列を、-f 2 として次の文字列を取り出している。
sed コマンド (セド、エスイーディー)
文字列を読み込み、正規表現に合致する文字列を加工する。

置換コマンド s の指定方法は s/正規表現文字列/取り替える文字列/g となっている。
(g は複数の意味。見つかった文字列すべてを対象にしたいときに記述する)

正規表現文字列に合致した部分文字列を取り替える文字列に挿げ替える。

ここでは、
s/[\r\n]\+//g として改行コードの削除を、
s/^GET\s\///g として行頭の "GET /" を削除している。
している。(取り替える文字列に何も指定していないため、削除となる)
touch コマンド (タッチ)
指定したファイルの更新日時を最新 (現在の日時) に変更する。
ファイルが存在しない場合は、空で新規作成する。
*9ここではすべての説明をしていません。気になる向きは (英語だけど) man コマンドを実行してください (例: man curl)。JM_Project は見えなくなりました