KumonoIto
はじめに
当初のバージョンはマルチスレッドウェブサーバであった。
スレッド = 糸
ウェブ = 蜘蛛の巣
これが名前の由来である。
目次
- 仕様(課題内容)
- 実装解説
- 並行サーバと反復サーバの比較
- forkの速度計測
実装解説
おもな機能
レポート提出時の機能(ver 1.0)
- GETのみ可能なWEBサーバ
- 並行サーバと反復サーバをコマンドラインで指定可能
- CGI実行可能
レポート提出後に追加した機能(ver 1.1)
- 設定ファイルにより以下にあげる項目が設定可能
- 外部コマンドによってディレクトリインデックスが生成可能
- 外部コマンドによってエラーメッセージが生成可能
- ファイル拡張子によるハンドラコマンドが設定可能
- アクセス制御設定可能
(ver 1.2)
- setPrefixFilterコマンドの追加
(ver 1.3)
- Bug fix
(ver 1.4)
- Bug fix
今後の実装予定(このサイトでの必要に応じて)
- 認証
- POST
ソース
- バージョン1.0
- kumonoito-1.0.tar.gz
- バージョン1.1
- kumonoito-1.1.tar.gz
- バージョン1.2
- kumonoito-1.2.tar.gz
- バージョン1.3
- kumonoito-1.3.tar.gz
- バージョン1.4
- kumonoito-1.4.tar.gz
インストール
通常のGNUパッケージと同様の手順に従う。
$ gtar zxf kumonoito-1.1.tar.gz
$ cd kumonoito-1.1
$ ./configure
$ gmake
$ sudo gmake install
起動方法
kumonoitod port configfile documentroot [-nofork] &
- port
- ポート番号
- configfile
- 設定ファイルのパス名
- documentroot
- クライアントから'/'で参照されるディレクトリのパス名
- -nofork
- このオプションを指定しない場合、1回のリクエストごとに新規に
プロセスを生成する。このオプションを指定すると、1プロセスです
べてのリクエストを逐次処理する。
設定ファイルの記述方法
設定ファイルは変更後、即座に反映される。サーバの再起動は不要。
設定ファイルの構文はつぎのとおり。
configfile ::= line*
line ::= pathprefix command arg* '\n'
| '\n'
| '#' .* '\n'
- pathprefix
- コマンドの適用対象となるパスを指定する。
リクエストされたURIのパス名の先頭がpathprefixに一致する場合に、
コマンドが適用される。
- command
- 現在使用可能なコマンドはつぎのとおり。
- setPrefixFilter
pathprefix setPrefixFilter filepattern handlercommand
ファイル名がfilepatternにマッチした場合、「そのファイル名
が実在するかチェックする前に」handlercommandを実行する。
- setHandler
pathprefix setHanlder filepattern handlercommand
ファイル名がfilepatternにマッチし、
かつ「そのファイルが実在する場合」(仕様が変?)、
handlercommandに指定されたコマンドをCGIスクリプトとして実行し、
その出力をクライアントに送る。
- setLimit
- アクセスの可否を制御する。
pathprefix setLimit (deny | allow) addresspattern
リクエストされたファイル名がfilepatternにマッチし、
かつクライアントアドレスがaddresspatternにマッチする場合に効力を持つ。
denyが指定されている場合、そのクライアントからのアクセスは拒否される。
allowが指定されている場合、アクセスを認める。
設定ファイルの先頭に記述された設定値が後に記述された設定値より優先される。
- setIndexHandler
pathprefix setIndexHandler handlercommand
ディレクトリがリクエストされた場合、
handlercommandが指すファイルがCGIスクリプトとして実行される。
- setErrorHandler
pathprefix setErrorHandler handlercommand
クライアントからのリクエストに対してエラーを通知する場合、
handlercommandが指すファイルをCGIスクリプトとして実行して
その出力をクライアントに返す。その際、エラーコードがスクリ
プトへのコマンドライン引数として渡される。
'#'以降はコメントとみなされ無視される。
設定ファイルの例
# 拡張子がshtmlであるファイルへのリクエストはdoshtml.cgiが処理する。
/ setHandler *.shtml doshtml.cgi
# is15e1u22からの、/以下のファイルに対するリクエストは許可する。
/ setLimit allow is15e1u22
# 150.65.40.118からの、/software/以下のファイルに対するリクエストは拒否する。
/software/ setLimit deny 150.65.40.118
# 150.65.*.*にマッチするアドレスを持つクライアントからの、/以下に対するリクエストは許可する。
/ setLimit allow 150.65.*.*
# その他のクライアントからの、/以下に対するリクエストは拒否する。
/ setLimit deny *
# /以下のディレクトリを指すURIがリクエストされたら、makeindex.cgiがCGIスクリプトとして実行される。
/ setIndexHandler makeindex.cgi
# /以下のファイルに対するリクエストがエラーを発生させたら、/home/.../error.cgiがCGIスクリプトとして実行される。
/ setErrorHandler /home/i0005/kiyoshiy/private_html/cgi-bin/error.cgi
# すべてのリクエストをログファイルに記録する。
/ setPrefixFilter * /home/i0005/kiyoshiy/private_html/cgi-bin/log.cgi
|
テスト
サーバが動いていれば、以下のURLをリクエストすることでサーバをテストできる。ただし、IEやNNなどのブラウザからリクエストした場合、ノーマルケース以外では「ドキュメントにデータが含まれていません。」とダイアログが表示されるのみでエラーの詳細は判別できない。詳細を調べる場合はTELNETで確認。
- ノーマルケース
- http://is15e1u22.jaist.ac.jp:50119/software/kumonoito/index.html
- その他のユーザによる読み出し許可がない場合(
touch forbidden.html;chmod o-r forbidden.htmlにより作成)
- http://is15e1u22.jaist.ac.jp:50119/software/kumonoito/forbidden.html
- ディレクトリ参照
- http://is15e1u22.jaist.ac.jp:50119/software/kumonoito/
- シンボリックリンク参照(
ln -s index.html symlink.htmlにより作成)
- http://is15e1u22.jaist.ac.jp:50119/software/kumonoito/symlink.html
- 並行テスト用の巨大ファイル(
dd bs=1024 count=102400 if=/dev/zero of=giant.html により作成)
-
- 並行サーバの場合(以下のアンカーを2回連続してクリックすると、並行してファイルが転送されているのが確認できる。)
-
http://is15e1u22.jaist.ac.jp:50119/software/kumonoito/giant.html
- 直列サーバの場合(以下のアンカーを2回連続してクリックすると、最初のファイル転送がおこなわれている間は、2回目の転送が開始されないことが確認できる。)
-
http://is15e1u22.jaist.ac.jp:50120/software/kumonoito/giant.html
- CGIのテスト(スクリプトのソース)
-
掲示板
実装の解説
並行サーバ
tcpserv.cのmain関数において、acceptする度に子プロセスを生成して応答を任せている。
for ( ; ; ) {
clilen = sizeof(cli_addr);
newsockfd = accept(sockfd, (struct sockaddr *) &cli_addr, &clilen);
if (newsockfd < 0)
err_dump("server: accept error");
if(!fork()){ /* 子プロセスを生成。*/
/* ゾンビとなって溜っている子プロセスを消滅させる。
これを怠ったために、forkの実験中にマシンをリブートするはめになった。*/
while(0 < waitpid(-1,NULL, WNOHANG))
;
do_process(newsockfd); /* リクエストを処理する。 */
close(newsockfd); /* 子プロセス側でソケットを閉じる。*/
exit(0);
}
close(newsockfd); /* 親プロセス側では即座にソケットを閉じる。*/
}
リクエスト処理に関するデータ構造
1回のリクエストに関する情報を保持する構造体Requestを定義し、関数間でこれを引き渡すようにした。
typedef struct t_Request{
int sock; /* ソケット記述子 */
/* 以下の二つはgetLine関数のstatic変数としてもよいが,マルチスレッド化を考慮してここに含めた。*/
char* readbuffer; /* ソケットから読み込まれた文字列を保持する。*/
int consumed; /* readbufferの何文字目までが処理済であるかを示す。*/
char* request; /* リクエストの最初の行 */
char* requesttokens[3];/* requestを空白で区切って得た文字列の配列 */
EMETHOD methodtype; /* Http METHODの種類(GETのみ) */
char* absolutepath; /* リクエストされたURLのファイルシステム上での位置 */
char* ext; /* ファイル拡張子 */
char* querystring; /* URLの'?'以降の文字列 */
char* contenttype; /* URLのファイル名の拡張子から判定されたContentType */
int filesize; /* リクエストされたファイルのサイズ */
}Request;
リクエスト処理の流れ
レポートの課題文に記述されているアルゴリズムに沿って実装した。
関数の多くは上記のRequest構造体を引数にとり、そのいくつかのフィールドを設定していく。
- request-lineの分解
getRequest関数
- methodの検査
checkMethod関数
- 要求パス名の検査と絶対パス名の生成
buildAbsolutePath関数
- 絶対パス名の存在検査/絶対パス名の通常ファイルのチェック/絶対パス名のパーミッション検査
checkFileAttribute関数
- 絶対パス名のタイプ判定
getContentType関数
- 絶対パス名のオープンまたはCGIスクリプト実行と出力
doResponse関数
(プロセス間の関連図)

各関数の解説
getRequest
getLine関数によりソケットから先頭の一行を読み取る。
読み取った行を空白をデリミタとしてトークンに切出し、各トークンを指すポインタを配列に格納する。配列の要素数は3固定とする。
checkMethod
メソッドが"GET"であることを確認する。
buildAbsolutePath
要求されたパスが"/"で始まることを確認する。それ以外ならばBadRequestエラーとする。
コマンドラインで指定されたドキュメントルートパスの末尾に、要求されたパスを連結して実パス名を得る。
checkFileAttribute
lstat関数を使用して実パス名が通常ファイルを指していることを確認する。通常ファイルでなければForbiddenエラーとする。通常ファイルであるかどうかはstruct stat構造体のst_modeをS_IFMTでマスクした値がS_IFREGと一致するかどうかで確認できる。
実パス名が指すファイルに対して、その他のユーザによる読み出し許可があることを確認する。struct stat構造体のst_modeのS_IROTHビットが立っているかどうかで確認できる。
実パス名が指すファイルサイズを取得する。ファイルサイズはstruct stat構造体のst_sizeより取得できる。
getContentType
実パス名から拡張子を得る。
得られた拡張子を"html","txt","jpg"とそれぞれ比較し、一致した場合はContentTypeを"text/html","text/plain","image/jpeg"に設定する。
その他の拡張子の場合、あるいは拡張子を持たないファイル名の場合、ContentTypeを"application/octet-stream"に設定する。
doRespones
拡張子が"cgi"の場合、popene関数を呼び出し、子プロセスで実行されるCGIプログラムとのパイプをFILE*形式で得る。
その他の場合、実パス名が指すファイルをfopenで開く。
応答ヘッダをwriteHTTPHeader関数によりソケットに出力する。
popeneあるいはfopenで得たFILE*からバイト列をread関数により読み出し、そのままwrite関数によりソケットに出力する。
ファイル内容をすべて出力したらファイルをクローズする。
writeHTTPHeader
引数で渡されたRequest構造体のcontenttype,filesizeから応答ヘッダを作成してソケットに出力する。
popene
機能的には、popen関数に環境変数も渡せるようにした関数。
第二回のレポートで作成した関数を流用した。
並行サーバと反復サーバの比較
実験方法
- 転送にある程度の時間がかかるようサイズの大きなファイルを用意する。
- ブラウザからそのファイルをリクエストする。
- 転送されている間に別のブラウザから同じファイルをリクエストする。
実験結果
テストファイルとして100MBのファイルを用意した。
反復サーバに対してこのファイルを2回リクエストした場合、最初のリクエストが完了するまで2回目のリクエストはブロックした。
(一方は転送中だが他方はブロックされている。)

(一方の転送が終り、他方の転送が開始された。)

並行サーバに対して同様のリクエストを送信した場合、最初のリクエストによるファイル転送と二回目のリクエストによるファイル転送が並行して進行していることが確認できた。
(ふたつの転送が同時進行している。)

forkの速度計測
実験方法
以下の制限を前提とする。
- サーバのソースには手を加えない。
- クライアントから確認できること。
上記の制限の元で以下の手順で実験をおこなう。
- サーバに対してリクエストをある回数連続して送信するスクリプトを用意する。
- そのスクリプトを用いて、並行サーバと反復サーバのそれぞれに対してリクエストを送る。
- 並行サーバの場合の処理時間から反復サーバの場合の処理時間を引いてリクエスト回数で割った時間を、forkにかかった時間とみなす。
実験結果
サーバへのリクエスト送信にはtcpcliを改造したプログラム(webperform.c)を使用した。このプログラムは引数で指定された回数、サーバへのconnectおよびcloseを繰り返す。
1000回の接続をテストした結果は以下の表のようになった。
| 対象サーバ | 1000回の接続に要した時間 |
| 並行サーバ | 0m48.671s | (a) |
| 反復サーバ | 0m8.331s | (b) |
したがって、
1回のforkに要する時間
= ((a) - (b)) / 1000
= 0.04s
となる。
(テストの実行図)
|