時代遅れひとりFizzBuzz祭り bash編おまけ(TCPクライアント/サーバ版)

id:eel3:20111031:1319989854 のおまけ。

bash 2.04以降では/dev/tcpや/dev/udpを使用してネットワーククライアントの処理を記述することができる。これらは実際に/dev以下にデバイスノードが存在する訳ではなく、bash上でのみ有効なシンタックス・シュガーのようだ(つまりOSの機能ではない)。

bashのネットワークサポートはnetcatのようにバックドアとして悪用される可能性がある為、ビルド時に有効/無効を切り替えられるようになっている。ディストリビューションによっては無効になっているようだ。

Ubuntu 10.04 LTSのbash 4.1.5では有効になっているので、試しにTCPクライアントアプリを書いてみた、FizzBuzzで。

#!/bin/bash

# fizzbuzz_tcp_client.sh

usage() {
    echo "Usage: `basename "$0"` [-h] [-p port] <hostname|ipaddr>" > "$1"
}

PORT=5432
OPT=

while getopts 'hp:' OPT; do
    case $OPT in
        h)
            usage /dev/stdout
            exit 0
            ;;
        p)
            PORT=$OPTARG
            ;;
        \?)
            usage /dev/stderr
            exit 1
            ;;
    esac
done
shift $(($OPTIND - 1))

if [ $# -ne 1 ]; then
    usage /dev/stderr
    exit 1
fi

exec 5<>/dev/tcp/$1/$PORT
if [ "$?" -ne 0 ]; then
    exit 1
fi

seq 1 100 >&5
echo 'EOR' >&5
cat <&5

# Because server close connection, we don't have to close.
# exec 5<>&-

改行区切りで数字を送信すると、サーバがFizzBuzzの答えを返すという想定だ。リクエストの終了は `EOR' という文字列を送信することで通知する。本当はC言語のソケットプログラミングでいう所の、

shutdown(SHUT_WR);

のようなことをしてサーバ側に送信終了(でも受信は継続中)を通知したかったのだが、うまくいかなかった。

で、肝心のサーバ側だが……bashのネットワークサポートはクライアント機能のみでサーバは無理だった。しかしもし `-e' オプションが有効なnetcatを使える環境ならシェルスクリプトでサーバを実装できる。例えばこんな感じ。

#!/bin/bash

# fizzbuzz_tcp_server2.sh

usage() {
    echo "Usage: `basename "$0"` [-h] [-p port]" > "$1"
}

PORT=5432
OPT=

while getopts 'hp:' OPT; do
    case $OPT in
        h)
            usage /dev/stdout
            exit 0
            ;;
        p)
            PORT=$OPTARG
            ;;
        \?)
            usage /dev/stderr
            exit 1
            ;;
    esac
done
shift $(($OPTIND - 1))

if [ $# -ne 0 ]; then
    usage /dev/stderr
    exit 1
fi

# We need to use traditional netcat
while nc -l -p $PORT -e `dirname $0`/fizzbuzz_tcp_server_core.sh; do
    :
done

Windows版のnetcatにはセッション終了時に再listenする `-L' オプションがあるが、Unixのnetcatにはそんなオプションが無いので接続が切れると終了してしまう。なのでループで再実行するようにしている。

`-e' オプションに指定するbashスクリプトの中身はこんな感じ。標準入力が受信に、標準出力が送信になる。

#!/bin/bash

# fizzbuzz_tcp_server_core.sh

pfizbuz() {
    case "$(($1 % 3)) $(($1 % 5))" in
        0\ 0 )  echo FizzBuzz ;;
        0\ * )  echo Fizz ;;
        *\ 0 )  echo Buzz ;;
        * )     echo $1 ;;
    esac
}

while read REQ; do
    if [ "$REQ" = EOR ]; then
        break
    elif (("$REQ")); then
        pfizbuz $REQ
    else
        echo '?'
    fi
done

オリジナルのnetcatのソースを見たところ `-e' オプションの値を何も加工せずにexecl(3)に突っ込んでいた。本当は `-e' の引数に `/bin/bash -c' を使って上記スクリプトの中身を直接書いてしまいたかったのだが、netcat本体が対応していないので無理なようだ。

ビルド時に `-e' オプションが無効化されたnetcatや、手元のUbuntuのように `-e' オプションが元々存在しないOpenBSD netcatの場合はどうだろうか? 残念ながら固定データを返す方法ならともかくリクエスト内容に応じて異なるレスポンスを返す方法は分からなかった。

なのでこんなシェルスクリプトでお茶を濁してみた。

#!/bin/bash

# fizzbuzz_tcp_server.sh

usage() {
    echo "Usage: `basename "$0"` [-h] [-p port]" > "$1"
}

PORT=5432
OPT=

while getopts 'hp:' OPT; do
    case $OPT in
        h)
            usage /dev/stdout
            exit 0
            ;;
        p)
            PORT=$OPTARG
            ;;
        \?)
            usage /dev/stderr
            exit 1
            ;;
    esac
done
shift $(($OPTIND - 1))

if [ $# -ne 0 ]; then
    usage /dev/stderr
    exit 1
fi

gawk -v service="/inet/tcp/$PORT/0/0" '
BEGIN {
    for (;;) {
        rc = (service |& getline)
        if (rc < 0) {
            close(service)
            break
        }
        if ((rc == 0) || ($0 ~ /^[ \t]*EOR[ \t]*$/)) {
            close(service)
            continue
        }
        if ($0 ~ /^[ \t]*[1-9][0-9]*[ \t]*$/) {
            s = ($0%3 == 0) ? "Fizz" : ""
            if ($0%5 == 0) {
                s = s "Buzz"
            }
            print (s == "") ? $0 : s |& service
        } else {
            print "?" |& service
        }
    }
}'

gawkのネットワークプログラミングって、何となくbashでのそれに似ている気がする。

まあそれにしてもC言語BSDソケットでネットワークプログラミング入門した身としては、TCP/IP限定とはいえ随分と気楽に通信ツールを書けるなあと感心するところだ。

もちろん例えばRubyでも通信プログラムは結構簡単に書けるし、gawkも最初は少々驚いたし扱い方に悩んだけどやはり簡単に書ける。ただbashの場合は手軽さとは別の次元で感心する所がある。それはシンタックスシュガーとはいえ通信相手を/dev/tcpや/dev/udpで始まるファイルとして抽象化している所だ。

元々Unixではシステム内の色々なものがファイルとして抽象化されていた。dd(1)でディスクから生データを吸い出すのが良い例だ。もっとも元から全てがファイル化されていた訳ではないし*1、後付されたX Window SystemBSDソケットはファイルシステムには含まれなかった。ハードウェアの制御もioctl(2)などが欠かせない。実はファイルへの抽象化を推し進めたのがPlan 9だったりする*2のだが、それはさておき。

Unix(というかLinuxFreeBSD)を触っている身としてはファイル操作は比較的身近なことだ。特にテキストの操作や加工は日常的な作業だといえる。なのでファイル操作の感覚でTCPUDPを扱えると、テキストベースのプロトコルを使ってシェル上から色々なことができて便利なのだ。

*1:例えばプロセスとか。

*2:もちろんPlan 9がやったこと/やっていることはそれだけではない。