|
C MAGAZINE Linux programming Tips
第12回 ソケットプログラミング道 - スモールWEBサーバーの実装 -
■はじめに
今回は、ソケットプログラミングの応用で、WEBサーバの実装について説明したいと思います。
そういえば最近、インターネットではアイディア的なプロトコルが登場しているような気がします。例えば、CDDBは良い例です。ご存知の方は多いと思いますが、 CDDBは、CDのヘッダー情報からアルバムのタイトル、曲名やアーティスト名などの情報を得るため、 あるいは逆にCDの情報を登録するためのプロトコルです。プロトコル的には、そんなに高度なものではないと思うのですが、 非常に便利なものです。インターネットが普通の生活の一部になってしまった現在では、ソケットプログラミングの知識とアイディアがあれば、 誰でも有名なプロトコルの産みの親になれるチャンスがありそうですね。
"プロトコル"、よく聞かれる言葉だと思います。今回は、WEBサーバーの実装なので、もちろんプロトコルはHTTP(Hyper Text Transfer Protocol)になります。今回は、 ソケットプログラミング自身のテクニックの説明というより、どちらかというと"プロトコル"をどのように扱うかの説明に近いと思います。あくまで今回は、 プロトコルに関する処理を説明するのが目的ですので、 HTTPのプロトコルの一部しか対象になりません。ちなみにHTTPのプロトコルと関連の仕様をカバーしようとすると本が一冊(以上 )できてしまうでしょう。
■スモールWEBサーバー
今回の例題としてあげるWEBサーバーは、ソースコードが全部17Kバイトぐらいの小さなものです。HTTPの一部しか対象でないと言ってしまったので、 がっかりした人もいるかもしれませんが、実は、簡単なメッセージボードとして動くCGIが組み込まれているのです。早速ですが、 この小さなWEBサーバーの実行画面を紹介します。どうですか、やる気が沸いてきましたか?
メッセージボード画面1
メッセージボード画面2
| |
とりあえずコンパイル実行してみよう!
動作を確認するのが理解の近道かもしれなので早速コンパイルして実行してみましょう。誌面には、 ソースコードが収まりきれないので、CD-ROMに全てのソースコードと背景などのJPEG画像が入っています。
[コンパイル]
$ tar xvfz esehttpd.tgz
$ cd esehttpd
$ make
[スモールWEBサーバの環境を準備]
$ make esedir <- /var/tmp/esehttpdに環境を作成
[スモールWEBサーバの実行]
$ ./esehttpd <- ポート番号が
ポート番号を指定したい場合は、
$ ./esehttpd 29999 |
もちろんクライアント(ブラウザー)は、netscapeとかになります(lynxでもかまいませんが...)。例えば、サーバのIPアドレスが、192.168.0.105で、 ポート番号が29389の場合、URLの欄に "http://192.168.0.105:29389/" と打ち込むとメッセージボードが現れます。
esehttpdは、httpのリクエストとレスポンスのログを標準出力に出力するようになっています。このログを追っているだけでもプロトコルの勉強になるでしょう。 注意深く観察してみましょう。
デバッグログ
....
[request] socket=4 len=14 'GET / HTTP/1.0'
[request] socket=4 len=25 'Host: 192.168.0.105:29389'
[request] socket=4 len=0 ''
[response] socket=4 'HTTP/1.0 200 Document follows'
[response] socket=4 'Content-Type: text/html'
[response] socket=4 'Content-Length: 534'
.... |
注意
- 全てのクライアントに対して正しく動作しないかも知れません。
Linuxのnetscapeとlynx、Windowsのnetscapeからはアクセスできました。
ただし文字コードの処理は省いているのでEUCとSJISの混在はできません。
- esehttpdは、tcp wrappersでアクセスコントロールしているので、
/etc/hosts.allow で esehttpdへのアクセスを許可してください。
/etc/hosts.allowの記述例
ALL : 127.0.0.1
sshd : ALL
esehttpd: 192.168.0.0/255.255.255.0
ALL : ALL : deny |
|
■HTTPプロトコル基本
HTTPプロトコルの基本を説明します。HTTPプロトコルには、正確にはバージョンがあり、バージョンごとに仕様が異なりますが、今回は、 現在標準だと思われる HTTP/1.0をベースに話しを進めます。
このプロトコルは、クライアント(netscapeなど)がサーバとコネクションを確立し、クライアントがリクエストを発行します。 そのリクエストに対してサーバは適切なレスポンスを返します。レスポンスを返したあとはコネクションが切断されます。HTTPでは、リクエストとレスポンス(データ以外)は、 人間が可読可能なテキストの形式です。逆にいうとプログラムのロジックにはやさしくありません。
ではどのようなやりとりが行われるか telnet コマンドを使って実験してみましょう。 どこでもいいですが適当な webサーバーに対して 以下のようにHTTPの GET を発行してみます。
GETコマンドの実験
$ telnet www.turbolinux.co.jp 80
GET / <- リクエストの発行
Trying 202.241.202.36...
Connected to www.turbolinux.co.jp.
Escape character is '^]'.
....
(とっても長いので省略)
....
Connection closed by foreign host. |
"GET /"は、"ルートのコンテンツをください"と意味のサーバに対するリクエストです。サーバーは、このリクエストに対してHTMLを返信しています。telnetでは、 単純に内容を出力しているだけですが実際は、 netscapeなどはこれを解析して描画するいたって大変なステージに入ります(サーバよりクライアントの実装の方が難しいのがHTTPの特徴 )。
実際は、クライアントからのリクエストは、もう少し複雑になります。リクエストを一般化すると以下のようになります。
リクエストの一般化
METHOD URL [HTTP/1.0]
HEADER1
...
HEADERn
<- 空行が必要
DATA... |
METHODは、リクエストの種類みたいなもので、GET,POST,HEAD,PUTなどがあります。METHODに続くURLは、文字どおりURLです。例えば、 クライアントがトップディレクトリーにあるa.jpgというファイルを得たい場合は、 "GET /a.jpg"というリクエストを発行することになります。以下の実際のリクエスト例を見て頂ければわかると思いますがHEADERは、 クライアントの情報などが含まれます。 HTTP/1.0のリクエストで最も注意しないといけないことは、ヘッダーとデータの間には必ず空行があることです。DATAは、 POSTなどでついてきます。空行のあとのヘッダー "Content-length:"で示される長さがDATAになります。
実際のPOSTリクエスト例
POST / HTTP/1.0
Referer: http://192.168.0.105:29389/
Connection: Keep-Alive
User-Agent: Mozilla/4.76C-ja [ja] (X11; U; Linux 2.2.18-2smp i686)
Host: 192.168.0.105:29389
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, image/png, */*
Accept-Encoding: gzip
Accept-Language: ja, en
Accept-Charset: iso-8859-1,*,utf-8
Content-type: application/x-www-form-urlencoded
Content-length: 65
body=%A5%E1%A5%C3%A5%BB%A1%BC%A5%B8%A4%CE%C6%FE%CE%CF&post=submit |
サーバは、このようなリクエストを受け取り内容を解析し、レスポンスします。妥当なリクエストの場合、"HTTP/1.0 200 OK"のような文字列を返します。 HTTP/1.0は固定で、続く200はステータスコードです。ステータスコード200というのは、クライアントに対する成功の通知です。 続く"OK"は、コメントみたいなものです。ステータスコードは、エラー応答などのコードが多く存在します。 インターネット上にHTTPプロトコルの仕様が転がっていると思いますので探してみてください。
以下は、esehttpdによるレスポンスです。
esehttpdによるPOSTへのレスポンス例
HTTP/1.0 200 OK
Content-Type: text/html
Content-Length: 534
(データ省略) |
| |
HTTPの処理コード
ここまで大雑把ですがHTTPの動作を説明しました。どうですか分かりましたか? 実際にコードを見た方が理解が早い人もいるのでesehttpdのHTTPを処理するコードを以下に載せます。
HTTPの処理コード: esewebserver.c
#include "common.h"
void ese_message_board(int s,char* from, char* rootdir)
{
char request[BUFSIZ];
char post_data[BUFSIZ];
char url[1024];
char url2[1024];
char useragent[1024];
int n;
int content_length=0;
int got_post=0;
size_t size;
if (chdir(rootdir)) {
ese_html_error(s,"400 Server Error",
"could not do chdir - please specify correct path");
return;
}
for( ;; ) {
n = recv_http_request_line(s, request, sizeof(request));
if( n==0 ) {
if( got_post ) n=recv_http_request(s, post_data, content_length);
break;
} else {
if ( method_match( request, "GET", url ,1024) ) ;
else if ( method_match( request, "POST", url ,1024) ) {
got_post = 1;
} else if ( method_match( request, "PUT", url ,1024) ) {
ese_html_error(s,"400 Bad Request",
"This server does not accept PUT requests");
return;
} else if (strncasecmp(request,"Content-Length: ", 16)==0) {
content_length = atoi(&request[16]);
} else if (strncasecmp(request,"User-Agent: ", 12)==0) {
strncpy( useragent, &request[12] , 1024);
} else {
/* skip all other requests... */
}
}
}
if( got_post ) {
char message[BUFSIZ];
extract_value(post_data,"body=", "&post=submit",
message, sizeof(message));
sprintf(url2, "%s/%s", rootdir, "logs.html");
append_message_to_file(url2, message,
"from %s (%s)", from, useragent);
}
if (!url) {
ese_html_error(s,"400 Bad Request",
"You must specify a GET or POST request");
return;
}
sprintf(url2, "%s%s", rootdir, url);
send_http_response(s,"HTTP/1.0 200 OK\r\n");
if( url[0] == '/' && strlen(url)==1) {
char pagebuf[BUFSIZ];
size = generate_main_page(pagebuf,sizeof(pagebuf));
send_http_response(s,"Content-Type: %s\r\n", "text/html" );
send_http_response(s,"Content-Length: %d\r\n", size);
send_http_response(s,"\r\n");
write( s, pagebuf, size);
} else if( access(url2,R_OK)==0 ) {
char content_type[12];
if( get_file_info(url2, content_type, &size ) ) {
send_http_response(s,"Content-Type: %s\r\n", content_type );
send_http_response(s,"Content-Length: %d\r\n", size);
send_http_response(s,"\r\n");
send_file(s,url2);
} else {
ese_html_error(s,"404 File Not Found",
"No such file or directory");
return;
}
}
return;
} |
esewebserver.cの流れ
step-1. METHODとHEADERの解析を行う
step-2. METHODがPOSTの場合、空行の後のデータをContet-lengthの長さで読み込む
step-3. METHODがPOSTの場合、logs.htmlに入力されたテキストを追加する
step-4. "HTTP/1.0 200 OK"を発行
step-5. urlが"/"の場合、メインページを出力
step-6. urlが"/"の場合、urlに対するファイルを送信 |
コードを見ればわかりますが、esehttpdのHTTPの処理コードでは、METHODは、 GETとPOSTのみしか処理していません。 また真面目にリクエストのヘッダーを読んでいません(そのあたりが"えせhttpd"の結縁)。もう一つ"えせ"な点として、 extract_value()関数のところで、CGIに対する処理を "body=", "&post=submit"のようにハードコードしています。 本来ならHTTPのFORMの内容に対して動的に処理しないといけないのですが、処理が繁雑になるので割愛しました。
POSTの処理
if( got_post ) {
char message[BUFSIZ];
extract_value(post_data,"body=", "&post=submit",
message, sizeof(message));
sprintf(url2, "%s/%s", rootdir, "logs.html");
append_message_to_file(url2, message,
"from %s (%s)", from, useragent);
} |
HTTPの処理コードで、urlが"/"の場合は、関数 generate_main_page()でメインページのHTMLをバッファーに一度ためてからクライアントに出力するようになっています。 このメインページは、ファイルとしてでなく、Cソースの中に埋め込まれています。main_page.c(CDROM)を参照してください。このページソースは、 以下のようになります。ちなみにCGIの変更は、main_page.c内のHTMLと esewebserver.cのPOSTの処理を変更することで行います。
メインページ
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2//EN">
<HTML>
<HEAD>
<TITLE>ese http server</TITLE>
</HEAD>
<BODY background="back.jpg" >
<center><img src="title.jpg"></center>
<hr>
<table><tr><td BGCOLOR="#eeeeee">
<FORM method=post>
<tr><td><TEXTAREA NAME="body" ROWS="4" COLS="50" WRAP="virtual"></TEXTAREA></td></tr>
<tr><td><INPUT TYPE="SUBMIT" NAME="post" VALUE="submit"></td>
<td><INPUT TYPE="RESET" VALUE="reset"></td></tr>
</FORM>
</TD></TR></TABLE></CENTER>
<hr>
<a href="logs.html">Logs</a>
<hr>
</BODY>
</HTML> |
このメインページには、わざとjpeg画像を入れてみました(見栄えのためだけでなく)。クライアントからは、最初は、 "GET /"のリクエストしかないのになぜ画像がクライアント(ブラウザー)に表示されているのでしょうか?実は。クライアントはメインページを受け取り、 ページを解析します。そのときに "back.jpg", "title.jpg"がないことに気づき、再度サーバーにファイル取得要求を発行します。サーバーは、 そのリクエストに対してファイルをクライアントに送信することになります。esewebserver.cの後半部分のコードがそれにあたります。
ファイルの送信
......
} else if( access(url2,R_OK)==0 ) {
char content_type[12];
if( get_file_info(url2, content_type, &size ) ) {
send_http_response(s,"Content-Type: %s\r\n", content_type );
send_http_response(s,"Content-Length: %d\r\n", size);
send_http_response(s,"\r\n");
send_file(s,url2);
} else {
ese_html_error(s,"404 File Not Found",
"No such file or directory");
..... |
|
■esehttpd 全体の構造について
若干ソースコードが多いため一部しか誌面に載せることができませんでした。 CDROMには、全ソースコードをまとめたtarファイル esehttpd.tgzがあると思います。 ソケットに関する処理の部分は、前回のソケットプログラミングで紹介した内容とほぼ同じものになっています。 esehttpdの全体の構造を把握しやすくできるように 各関数の呼び出し関係を図にしてみました。
mainの流れ
webサーバ内の流れ
■さいごに
このesehttpdは、CGIもサポートしていますが、特別に外部プログラムを要求しません。 もちろんソケットの処理自分自身でおこなっているのでinetdがなくても動作します。カーネルが起動して正しくネットワークインターフェイスが設定されていれば、 esehttpdは動作します。ライブラリーをスタティックリンクした場合、esehttpdは、 400Kバイトぐらいのサイズになります。ワンフロッピー Linuxのワンフロッピー化の知識があれば(今度、機会があれば説明したいと思います)、 フロッピーをコンピュータの入れて電源を入れると簡易メッセージボードのサーバになってしまうのです。
特に最近は、専用ハードウエアの組み込み用途のOSとしてLinuxは非常に注目されています。そのような専用ハードウエアは、 キーボードやモニタを持っていないのでほとんどがWEBコントロールになります。もしそのようなことを行う機会があれば、esehttpdの知識はきっと役立つことでしょう。
esehttpdのコードは、LinuxのSMBサーバのパッケージ samba の swatというプログラムの一部を参考にさせて頂きました。 samba/swatの開発者とオープンソースの存在には感謝いたします。
■プログラムへのリンク
Makefile
esehttpd.c
esewebserver.c
http_common.c
main_page.c
sep_token.c
acl.c
common.c
common.h
esehttpd.tgz
|