[ WEBサーバ 〜パイプによるCGIの実現〜 ] 動作確認はすべて Linux + gcc で行っています。 HTTP(Hyper-Text Transfer Potocol) は WWW(World Wide Web) で使用されて いるプロトコルです。一般にポート80番を使用します。HTTPに関する詳細は RFC1945, RFC2068, RFC2616などを参照してください。 この記事は ECHOサーバに関する記事(↓) の続きとして書かれています。 http://ruffnex.oc.to/kenji/xrea/echo.txt http://ruffnex.oc.to/kenji/src/echo.txt まずはソースコード(↓)です。これを元に話を進めます。 http://ruffnex.oc.to/kenji/src/webserver.c 基本は echo4.c ですが、WEBサーバ用にいろいろと書き変えています。 echoサーバは受け取った文字列を返すだけだがHTTP(WEB)サーバは文字列を受 け取り、それを加工し、クライアントが要求しているファイルを取得してそれ を返さなければなりません。よって特定文字列の切り取りや改行の削除などの 文字列関連の関数が必要になります。 まずは文字列関連の関数を説明します。 CutCrLf ------------------------------------------------------------------------------ void CutCrLf( char *str ) { char *p; if ((p = strchr(str, '\r')) != NULL) *p = '\0'; else if((p = strchr(str, '\n')) != NULL) *p = '\0'; } ------------------------------------------------------------------------------ これは受け取った文字列の改行を排除する関数です。strchrで'\r'もしくは '\n'のポインタを取得して存在したらその部分を'\0'に置き換えます。まぁ簡 単ですね。処理内容は見れば分かります。 CharSmall ------------------------------------------------------------------------------ void CharSmall( char *str ) { unsigned int i; for(i=0; str[i] != '\0'; i++) str[i] = (char)tolower((int)str[i]); } ------------------------------------------------------------------------------ 文字列を小文字に変換する関数です。文字列比較するときにこの関数に渡して 小文字に変更したあとに比較します。大文字、小文字を区別しない文字列比較 のときに役立ちます。 CutChar ------------------------------------------------------------------------------ int CutChar( char *data, char *str, char c ) { unsigned int i; for(i=0; data[i] != c; i++) str[i] = data[i]; str[i] = '\0'; return i; } ------------------------------------------------------------------------------ 任意の文字が見つかるまでの文字列をstrにコピーする関数です。strtokに似 た(あくまで似た)処理を行うことができます。戻り値は任意の文字列が見つかっ た場所(先頭から何番目か)です。 とりあえず文字列関連の関数はこれだけです。簡単ですね。では次はmain関数 から呼ばれてるOneClientを見てみます。 OneClient ------------------------------------------------------------------------------ int OneClient( int acc ) { static int count = 1; switch(fork()){ case 0: if(DoClient(acc) < 0) fprintf(stderr, "DoClient\n"); close(acc); exit(EXIT_SUCCESS); break; case -1: close(acc); fprintf(stderr, "fork"); break; default: close(acc); if(count >= MAX_CONNECT) wait(0); else count++; } return 0; } ------------------------------------------------------------------------------ echo4.c と比べると多少変更されています。まず同時接続数の制限をしていま す。クライアントの最大数は MAX_CONNECT で 43行目に define しています。 初期設定は 2 ですので、適当に変更してください。(さすがに 2 は少ないで すね) 最初に static で count を宣言して初期値を 1 としています。仮に MAX_CONNECTが 5 とすると、親プロセスの処理(つまりはdefault:以下)はelse に入り count++ が実行されて終了します。(count = 2)そして次の接続要求が 来たときにまた OneClient が呼ばれます。MAX_CONNECT は 5 なのでまた親プ ロセスは else が実行されます(count = 3, 相手にしているクライアント数 2) これを続けて行くと count = 5, 相手にしているクライアント数 4 という状態 で5回目の接続要求を受けたときに OneClient が呼ばれることになります。今 度は count >= MAX_CONNECT が真なので wait(0) が実行されることになりま す。この時点で相手にしているクライアント数は 5 であり count = 5 です。 もしも今までの接続要求に対して作られた子プロセス5つのどれか1つ以上が終 了(exit)しているならば wait がその終了している子プロセスを確認して通過 します。しかしもし、今までの接続要求に対して作られた子プロセスがどれも 終了していないならば wait によってどれか1つの子プロセスが終了するまで 処理が止まります。いずれかの子プロセスが終了しなければ親プロセスは accept まで進めないので、新たな接続要求には対応できません。そしてこれ からはcountの値は変わりません。elseの処理に入ることもありません。子プ ロセスが5つとも終了したら終了したプロセスが5つ存在することになり、wait が5回通過できるようになります。子プロセスが1つ終了すれば wait が1回 通過できるようになる。これにより同時接続数の制限を実現しています。 では次は子プロセスで実行されるDoClient関数を見てみます。 DoClient ------------------------------------------------------------------------------ int DoClient( int acc ) { FILE *sock_fp; int loop = TRUE; fd_set rfds; struct timeval timeout; if((sock_fp = fdopen(acc, "r+")) == NULL){ fprintf(stderr, "fdopen\n"); return(-1); } setvbuf(sock_fp, NULL, _IONBF, 0); while(loop){ FD_ZERO(&rfds); FD_SET(acc, &rfds); timeout.tv_sec = SET_TIMEOUT; timeout.tv_usec = 0; switch(select(acc + 1, &rfds, NULL, NULL, &timeout)){ case -1: fprintf(stderr, "select\n"); return(-1); break; case 0: /* timeout */ loop = FALSE; break; default: if(TalkClient(sock_fp) != FALSE) loop = FALSE; break; } } fclose(sock_fp); return 0; } ------------------------------------------------------------------------------ この関数はあまり変更が無いです。SET_TIMEOUT で timeout の時間を設定で きるようにしているくらいです。あと TalkClient関数 の戻り値がFALSEなら loopをFALSEにする(つまり切断)という処理をしていますがこれは接続維持と いうHTTPの仕様に対応しようと頑張ってるということです(笑)それを説明す るために次は TalkClient関数の説明をします。 TalkClient ------------------------------------------------------------------------------ int TalkClient( FILE *sock_fp ) { char *data[64], buf[2048]; HEAD_DATA headData; int close_flag = TRUE; unsigned int len; unsigned int i, k; /* ヘッダの取得 */ for(i=0; i < 64; i++){ fgets(buf, sizeof(buf), sock_fp), (void)CutCrLf(buf); if((len = strlen(buf)) == 0) break; if((data[i] = (char *)calloc(len + 1, sizeof(char))) == NULL){ fprintf(stderr, "calloc\n"); return(-1); } strcpy(data[i], buf); } k = i; len = CutChar( data[0] , headData.method, ' ' ), len++; len += CutChar( data[0] + len, headData.path, ' ' ), len++; len += CutChar( data[0] + len, headData.version, '\0' ), len++; if(strstr(headData.version, "1.0") != NULL || strstr(headData.version, "1.1") != NULL) close_flag = FALSE; (void)CharSmall(headData.method); if(strcmp(headData.method, "get") == 0){ if(GetHtml(sock_fp, &headData) == TRUE) close_flag = TRUE; } for(i=0; i < k; i++) free(data[i]); return(close_flag); } ------------------------------------------------------------------------------ HEAD_DATA構造体はソースの最初に定義しています。HTTPの METHOD, ファイル パス, バージョンを持たせます。 まずはHTTPヘッダ情報を取得しますが、64行以上ヘッダがある場合はそれ以上 は無視します(笑)というか64行以上もヘッダが必要な状態が果たしてあるの かと思いますが、まぁ気になる方は128でも256でも適当に設定してください。 さらに1行のヘッダ情報につき2048bytes以上は無視します。これも2048バイ ト以上も必要なことがあるだろうか?ということですが、CGIなどでクエリ文 字列に掲示板のデータなどを渡す時にあるかも知れないです。が、そんなもん はPOSTで実現してくださいということで終了!(ちなみにwebserver.cではPOST に対応してません) close_flagというのはこの関数が終了したら切断するかしないかという意味を 表しています。(FALSE = しない、TRUE = する)close_flagの初期値は TRUE なので切断するということで処理が進みますが、もしもversionが1.0以上なら ば切断しないようにします。これは1.0から接続維持という仕様ができたから ですが詳しいことは分かりません。しかしもしGetHtmlの戻り値がTRUEなら再 びclose_flagをTRUEにします。これを説明するために次はGetHtml関数の説明 をします。 GetHtml ------------------------------------------------------------------------------ int GetHtml( FILE *sock_fp, HEAD_DATA *data ) { FILE *fp; char *buf; struct stat st; if(stat(data->path + 1, &st) < 0){ Error404(sock_fp, data); return(TRUE); } if((buf = (char *)calloc(st.st_size, sizeof(char))) == NULL){ fprintf(stderr, "calloc\n"); return(-1); } fprintf(sock_fp, "%s 200 OK\r\n", data->version); fprintf(sock_fp, "Server:sample\r\n"); fprintf(sock_fp, "Content-Type:text/html\r\n"); fprintf(sock_fp, "Content-Length:%d\r\n", (int)st.st_size); fprintf(sock_fp, "\r\n"); if((fp = fopen( data->path + 1, "r")) == NULL){ fprintf(stderr, "fopen\n"); return(-1); } fread(buf, 1, st.st_size, fp); fwrite(buf, 1, st.st_size, sock_fp); free(buf); fclose(fp); return(FALSE); } ------------------------------------------------------------------------------ ここはあまり説明するところがありませんが、まずはstatで要求されたファイ ルの情報を取得します。もしもファイルが無い時はError404に処理を渡して TRUEを返します。TRUEを返すということは切断するということですので、エラー ならば切断するということです。もしファイル情報が取得できたなら成功した というHTTPヘッダを送ってそのあとファイルを開いてデータをsock_fpに渡し ています。何故fgets,fprintfではなくfread,fwriteを使っているのかという と、関数名はGetHtmlですが、実際要求されるファイルはHTMLファイルだけで は無く(CGIはまた別ですよ) gif, png といった画像データも要求されるから です。gets,fprintfだと画像データを文字列として取得してしまうので、ここ だけfread, fwriteを使用しています。 最後にちょっとした関数を紹介。 setSignal ------------------------------------------------------------------------------ void setSignal( void ) { signal(SIGPIPE, SIG_IGN); signal(SIGTTIN, SIG_IGN); signal(SIGTTOU, SIG_IGN); signal(SIGINT, EndProg); signal(SIGTERM, EndProg); } ------------------------------------------------------------------------------ シグナルの設定を関数として分けました。SIG_IGN とは「無視」ということで す。SIGINT と SIGTERM の時は EndProg関数 を呼びます。 EndProg ------------------------------------------------------------------------------ void EndProg( int sig ) { if(pPid == getpid()) sleep(1); fprintf(stderr, "STOP! --> sig = %d --> getpid = %d\n", sig, getpid()); exit(EXIT_SUCCESS); } ------------------------------------------------------------------------------ pPid はグローバル変数です。最初に親プロセスのプロセスIDを保存しておく 変数です。もし、親プロセスがSIGINTを受け取ったら一秒待ちます。これは親 プロセスが子プロセスより先に終了してしまうのを防ぐためです。別に先に終 了してもプログラムとしては問題無いのですが、ちょっと個人的に気になった ので。 Error404 ------------------------------------------------------------------------------ void Error404( FILE *sock_fp, HEAD_DATA *data ) { fprintf(sock_fp, "%s 404 Not Found\r\n", data->version); fprintf(sock_fp, "Server:sample\r\n"); fprintf(sock_fp, "Connection:none\r\n"); fprintf(sock_fp, "Content-Type:text/html\r\n"); fprintf(sock_fp, "\r\n"); fprintf(sock_fp, "\r\n" "\r\n" "404 Not Found\r\n" "\r\n" "\r\n" "File is not found.\r\n" "\r\n" "\r\n" ); } ------------------------------------------------------------------------------ ファイルが存在しない時のレスポンスです。 本当はレスポンスヘッダにContent-Lengthを加えた方がいいのですが、めんど くさいので止めました(笑)しかしこれを加えないとブラウザがどこがデータ のの終端なのかを判断できなくなりますので、ブラウザは切断処理をせずにサ ーバ側のタイムアウトになるまで接続を維持してしまいます。これだとちょっ と困るのでエラーの場合はこちら側から強引に切断する仕様にしました。よっ てError404関数が呼ばれた後はTRUEを返して、close_flagをTRUEにしてこちら 側から切断するようにしています。でも本当はちゃんとContent-Lengthを渡し た方が良いと思われます。 では実際にプログラムを実行してみます。 index.html ---------------------

TEST!!!

--------------------- というファイルを同じディレクトリに置きます。もちろんwb.gifも同じディレ クトリに置きます(ちなみにwb.gifとはWizardBibleのバナーです) [kenji@localhost web]$ ls index.html wb.gif webserver.c [kenji@localhost web]$ [kenji@localhost web]$ gcc -Wall webserver.c -o webserver [kenji@localhost web]$ su Password: [root@localhost web]# ./webserver 80 Ready for Accept accept:127.0.0.1:32925 (別のWindowにて) [kenji@localhost kenji]$ telnet localhost 80 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. get /index.html HTTP/1.0 HTTP/1.0 200 OK Server:sample Content-Type:text/html Content-Length:65

TEST!!!

Connection closed by foreign host. [kenji@localhost kenji]$ ブラウザでも無事閲覧できました。 http://ruffnex.oc.to/kenji/src/web.gif では次は CGI にも対応してみます。CGIに対応するということは POST にも対 応しなければならないということになり、うわっメンドクサ...とか思ったの ですが、まぁ仕方ないので頑張ります。(ぉぃ) では、まずはパイプの話から。 pipe.c ------------------------------------------------------------------------------ #include #include #include int main(void) { int pfd[2]; char str[64]; unsigned int i; for(i=0; i < 64; i++) str[i] = '\0'; if(pipe(pfd) == -1) perror("pipe"); switch(fork()){ case -1: perror("fork"); break; case 0: close(pfd[0]); write(pfd[1], "test", 5); exit(0); break; default: close(pfd[1]); wait(0); read(pfd[0], str, sizeof(str)); printf("%s\n", str); } return 0; } ------------------------------------------------------------------------------ [kenji@localhost server]$ gcc -Wall pipe.c [kenji@localhost server]$ ./a.out test [kenji@localhost server]$ 説明がとても難しいのですが、子プロセスがwriteでpfd[1]に対して書き込み を行っています。親プロセスは子プロセスの終了を確認した後にpfd[0]からデー タを読み込みます。そしてそれを出力しています。実行すると見事 test とい う文字列が出力されてますので pfd[1] が「書き込み」であり pfd[0] が読み 込みであることが分かります。そして pfd[1] と pfd[0] は繋がってるという 風に考えることができます。これを使うと別のプロセス同士でデータのやりと りができることになります。pfd[1] と pfd[0] がパイプで繋がっているとい う感覚です。 子プロセス || 親プロセス || pfd[1] || pfd[0] 書込 "test" → ==================================== → 読込 "test" || パイプを利用すると、別のプログラムの出力結果を取得することができます。 hello.c ------------------------------------------------------------------------------ int main(void) { printf("This program is hello.\n"); return 0; } ------------------------------------------------------------------------------ [kenji@localhost server]$ gcc hello.c -o hello [kenji@localhost server]$ ./hello This program is hello. [kenji@localhost server]$ pipe2.c ------------------------------------------------------------------------------ #include #include #include int main(void) { int pfd[2]; char str[64]; unsigned int i; for(i=0; i < 64; i++) str[i] = '\0'; if(pipe(pfd) == -1) perror("pipe"); switch(fork()){ case -1: perror("fork"); break; case 0: close(pfd[0]); dup2(pfd[1], 1); execl("./hello", "hello", NULL); exit(0); break; default: close(pfd[1]); dup2(pfd[0], 0); wait(0); read(pfd[0], str, sizeof(str)); printf("%s", str); } return 0; } ------------------------------------------------------------------------------ [kenji@localhost server]$ gcc -Wall pipe2.c [kenji@localhost server]$ ./a.out This program is hello. [kenji@localhost server]$ [kenji@localhost server]$ man dup2 ............... int dup2(int oldfd, int newfd); dup and dup2 create a copy of the file descriptor oldfd. dup2関数はファイルデスクリプタ(第一引数)の複製を作ります。と書いてある のですが、ぶっちゃけ、ちょっと意味が分からないです。なので推測で話しま す(ぉぃ) pfd[0] と pfd[1] はパイプで繋がれてます。標準入力 0 と 標準出 力 1 は繋がれてないので、pfd[1] をコピーしたものを標準出力 1 とする。 そしてpfd[0] をコピーしたものを標準入力 0 とすると 1 と 0 はパイプで繋 がれることになる。つまり 0 に入力したデータを 1 から取得することができ るようになる。ということかな。と私は思ってます(ぉぃぉぃ)でもあんまり分 かってないのでdup2関数を使うと標準入力と標準出力をパイプでつなげること ができる。ということだけおさえておけば問題無いかと。(なんか無責任だな) WEBサーバCGI対応バージョン(↓)です。 http://ruffnex.oc.to/kenji/src/wser_cgi.c POSTには対応してませんし、CGIを実行する場合も環境変数を渡してないので クエリ文字列なども渡せません(まだまだサーバとしては全然ダメですが) webserver.c に追加したいくつかの関数の説明をします。 まずTalkClientをほんの少し変更しています。これはCGIファイルならば ExeCgi関数に処理を任せるといったものなのでまぁ見れば分かると思います。 あとそのために HEAD_DATA 構造体も拡張子を保存するものを追加しています。 これをみて.cgiならばCGIファイルと判断してExeCgiに処理を渡すというよう なことをやってます。またエラーレスポンスとしてError403関数を追加してい ますが、Error404と同じですので説明はしません。 では ExeCgi関数を見てみます。 ExeCgi ------------------------------------------------------------------------------ int ExeCgi( FILE *sock_fp, HEAD_DATA *data ) { FILE *tmp; int pfd_in[2], pfd_out[2], pfd_err[2]; int fd_write, fd_read, fd_err; int W = 1, R = 0; unsigned int len, size, crlf_len; char buf[1024], content[16]; char *ptr; char *arg[16], *env[16]; struct stat st; if(stat(data->path + 1, &st) < 0){ Error404(sock_fp, data); return(TRUE); } if(pipe(pfd_in) == -1 || pipe(pfd_out) == -1 || pipe(pfd_err) == -1){ fprintf(stderr, "pipe\n"); return(TRUE); } switch(fork()){ case -1: fprintf(stderr, "fork\n"); break; case 0: close(pfd_in[W]); close(pfd_out[R]); close(pfd_err[R]); dup2(pfd_in[R], 0); /* stdin */ dup2(pfd_out[W], 1); /* stdout */ dup2(pfd_err[W], 2); /* stderr */ arg[0] = data->path + 1; arg[1] = NULL; env[0] = NULL; if(execve(data->path + 1, arg, env) < 0){ close(pfd_in[R]); close(pfd_out[W]); close(pfd_err[W]); fprintf(stderr, "execve\n"); } exit(0); break; default: close(pfd_in[R]); close(pfd_out[W]); close(pfd_err[W]); fd_write = pfd_in[W]; fd_read = pfd_out[R]; fd_err = pfd_err[R]; break; } wait(0); ...(続く)... ちょっと長いので分けます。 まずパイプは説明したとおりですが、ここでは3つ作っています。3つ作っても どうせ1つしか使ってないのですが一応今後のためにstdin, stdout, stderr とそれぞれ必要になるかもしれないからです。子プロセスはexecveを呼んでま すが環境変数や引数などは渡してないので、CGI側から環境変数を参照できま せん。よってこれは次のプログラムでPOST対応とともに解決しようと思ってま す。親プロセスは不必要なものをcloseしたのちbreakで抜け出して子プロセス が終了するのを待ちます。といっても子プロセスはexecveになってますので CGIの実行が終了するまで待つことになります。ここでもし終わらないCGIを実 行してしまったら、残念ながらプロセスは終了しませんので、親プロセスは止 まったままとなり、サーバがやばい状態になります。 ...(続き)... len = read(fd_read, buf, sizeof(buf) - 1); buf[len] = '\0'; strncpy(content, buf, 12), content[12] = '\0'; CharSmall(content); if(strcmp(content, "content-type") != 0){ Error403(sock_fp, data); return(TRUE); } crlf_len = 4; if((ptr=strstr(buf, "\r\n\r\n")) == NULL ){ crlf_len = 2; if((ptr=strstr(buf, "\n\n")) == NULL ){ if((ptr=strstr(buf, "\r\r")) == NULL ){ Error403(sock_fp, data); return(TRUE); } } } ...(続く)... CGIの実行結果から最初の Content-Type:text/html\r\n\r\n というやつがあ るかどうかを判断しています。無ければエラーとなり改行もちゃんと2つある かどうかを確認しています。ただしこのプログラムでは確認だけしか行ってな くあることが分かれば text/html と決め打ちしています。本当は text/plain などいろんなものに対応しなければならないのですが、なんさま難しいので勘 弁してください。 ...(続き)... tmp = tmpfile(); size = 0; len = len - ((ptr + crlf_len) - buf); fwrite(ptr + crlf_len, 1, len, tmp); size += len; while( (len = read(fd_read, buf, sizeof(buf))) > 0 ){ fwrite(buf, 1, len, tmp); size += len; } fprintf(sock_fp, "%s 200 OK\r\n", data->version); fprintf(sock_fp, "Server:sample\r\n"); fprintf(sock_fp, "Content-Type:text/html\r\n"); fprintf(sock_fp, "Content-Length:%d\r\n", size); fprintf(sock_fp, "\r\n"); rewind(tmp); while((len=fread(buf, 1, sizeof(buf), tmp)) > 0){ fwrite(buf, 1, len, sock_fp); } close(pfd_in[W]); close(pfd_out[R]); close(pfd_err[R]); fclose(tmp); return(FALSE); } ------------------------------------------------------------------------------ CGI実行結果をtmpファイルに一度全部格納してデータの長さを取得したあとあ らためてクライアントにデータを送信しています。tmpfile関数はちょっとお もしろそうなのでつかって見ました。なかなか便利です。知らない人は調べて みて下さい。データの長さを取得したら Content-Length にそれをいれて、レ スポンスを返しています。 test.cgi ------------------------------------------------------------------------------ #!/usr/bin/perl print "Content-type:text/html\r\n\r\n"; print " \n"; print " \n"; print "TEST!!!\n"; print ""; print "\n"; print "\n"; exit; ------------------------------------------------------------------------------ wser_cgi.c と同じディレクトリに置きます。 [kenji@localhost web]$ chmod 755 test.cgi [kenji@localhost web]$ gcc -Wall wser_cgi.c -o wser_cgi [kenji@localhost web]$ su Password: [root@localhost web]# ./wser_cgi 80 Ready for Accept accept:127.0.0.1:32963 (別のWindowで) [kenji@localhost kenji]$ telnet localhost 80 Trying 127.0.0.1... Connected to localhost. Escape character is '^]'. GET /test.cgi HTTP/1.0 HTTP/1.0 200 OK Server:sample Content-Type:text/html Content-Length:58 TEST!!! Connection closed by foreign host. [kenji@localhost kenji]$ ブラウザでも無事閲覧できました。 http://ruffnex.oc.to/kenji/src/web2.gif ではいよいよ POST 対応、環境変数設定バージョンのソースを見ていくことに します。最終的なWEBサーバ(↓)です。 http://ruffnex.oc.to/kenji/src/wser_cgi2.c まずソース全体にある程度の改良が加えられてます。環境変数は少ししか設定 してませんが、それはメンドクサイからです(笑)まぁ気になる方はお好きな ように追加してください。ポイントとなるところはコメントを入れました。こ れまでのことを理解しているならば十分自力で読めると思います。最終的にソ ースは600行を超えましたがまぁWEBサーバとしては全然短いでしょう。よって 勉強には最適かと思われます(^^; ちなみに CutChar関数 は無くなりました。 (HEAD_DATA構造体を全部ポインタにしたので) ではポイントだけ説明します。(左の数字は行数です) < TalkClient関数 > 288: if(strcmp(headData.method, "post") == 0){ /* Content-Lengthがあるデータを探す */ for(i=0; i < k; i++){ if(strncmp(data[i], "Content-Length", 14) == 0) break; } /* もし無ければおかしなリクエストなので切断 */ if(i == k) return(TRUE); /* 数字が書かれてあるとこのポインタを取得 */ if((ptr = strpbrk(data[i], "123456789")) == NULL) return(TRUE); /* その文字列を数に変換 */ len = atoi(ptr); /* len+1だけメモリを確保してポインタをdata[k]へ */ if((data[k] = (char *)calloc(len + 1, sizeof(char))) == NULL){ fprintf(stderr, "calloc\n"); return(-1); } fread(data[k], 1, len, sock_fp); headData.post_data = data[k]; headData.content_len = len; k = k + 1; }else{ headData.post_data = NULL; headData.content_len = 0; } methodがPOSTであった場合の処理です。ポイントはコメントとして書かれてま すが、もう少しくわしい説明をします。まずPOSTならば必ず Content-Length が存在するはずですので、それを探します。もし無い場合はそのまま接続を切 断します。存在するならばそれから数字の部分を取得してそれからメモリを確 保して実際にデータを受け取ります。受け取ったポインタと文字列の長さはそ れぞれ headData に格納します。POSTじゃないならばこれらは使わないので、 そのまま NULL と 0 を代入させて終了させます。 < ExeCgi関数 > 407: /* POSTならば標準入力にデータをいれといてやります */ if(strcmp(data->method, "post") == 0){ write(fd_write, data->post_data, data->content_len); } この部分はPOSTされたデータをCGI側の標準入力のパイプに渡しています。つ まりここで渡したデータをCGI側はSTDINから取得することができるということ です。ExeCgiでのGETとPOSTの違いはここだけです。 あたらしく追加した関数に環境変数のセット、解放、を行う関数を作りました。 setEnv ------------------------------------------------------------------------------ int setEnv( char **env, HEAD_DATA *head ) { unsigned int i = 0; char buf[2048]; sprintf(buf, "PATH=%s", head->path); env[i++] = strdup(buf); sprintf(buf, "SERVER_SOFTWARE=sample"); env[i++] = strdup(buf); sprintf(buf, "SERVER_PORT=80"); env[i++] = strdup(buf); sprintf(buf, "SERVER_NAME=testWEB"); env[i++] = strdup(buf); sprintf(buf, "REQUEST_METHOD=%s", head->method); env[i++] = strdup(buf); if(head->k_data != NULL){ sprintf(buf, "QUERY_STRING=%s", head->k_data); env[i++] = strdup(buf); } sprintf(buf, "CONTENT_LENGTH=%d", head->content_len); env[i++] = strdup(buf); env[i] = NULL; return i; } ------------------------------------------------------------------------------ これを実行すると引数のポインタに環境変数がセットされます。これはexecve が呼ばれる前に行います。ここでは少ししかセットしていませんが、気になる ならば、お好みの環境変数をセットしてください。HEAD_DATA の要素を増やす だけでいくらでもセットできると思います。strdup関数はなかなか便利な関数 で(私も始めて使ったんですが)引数に渡した文字列分のメモリを確保してくれ るというすぐれものです。mallocの文字列バージョンといったところかもしれ ないですが、ものすごく良いです。ただ確保したからにはちゃんと解放しなけ ればなりません。ここはmallocと同じです。 FreeEnv ------------------------------------------------------------------------------ void FreeEnv( char **env, unsigned int k ) { unsigned int i; for(i=0; i < k; i++) free(env[i]); } ------------------------------------------------------------------------------ 解放をする関数です。execveの後に実行されます。まぁ見れば分かりますが、 setEnvの戻り値を 引数 k に渡さなければなりません。 では最後に動作確認をします。 test4.cgi ------------------------------------------------------------------------------ #!/usr/bin/perl print "Content-type:text/html\r\n\r\n"; if( $ENV{'REQUEST_METHOD'} eq "post" ) { read( STDIN, $buf, $ENV{'CONTENT_LENGTH'} ); } print " \n"; print " \n"; print "

$ENV{'PATH'}

"; print "

$ENV{'REQUEST_METHOD'}

"; print "

$ENV{'SERVER_PORT'}

"; print "

$ENV{'SERVER_SOFTWARE'}

"; print "

$ENV{'SERVER_NAME'}

"; print "

$buf

"; print "

$ENV{'QUERY_STRING'}

"; print << "HTML";
mailaddr:
subject :


HTML print "\n"; print "\n"; exit; ------------------------------------------------------------------------------ wser_cgi2.c と同じディレクトリに置きます。 [kenji@localhost web]$ chmod 755 test4.cgi [kenji@localhost web]$ [kenji@localhost web]$ gcc -Wall wser_cgi2.c -o wser_cgi2 [kenji@localhost web]$ su Password: [root@localhost web]# ./wser_cgi2 80 Ready for Accept ブラウザでアクセスする。( http://localhost/test4.cgi?ttttetsertetsesssssss ) http://ruffnex.oc.to/kenji/src/web3.gif さらにデータをPOSTします。 http://ruffnex.oc.to/kenji/src/web4.gif 無事閲覧できると思います。 最後に 無事、WEBサーバができました。でもGETとPOSTにしか対応してませんし、WEB サーバとして最低限の機能しかついてません。それは何故かというと、私にあ まりHTTPに関する知識が無いからです(^^; 実際HTTPはバージョンを重ねるご とに様々な仕様を追加していき現在はかなり複雑なものとなっているようです が、私はHTTP自体あまり知らないのでそもそもサーバなんて作れるわけ無いの です(ぉぃ でもまぁ形になれば良いかなと思い、ちょっと作ってみたのです。 実際にサーバとして運営していこうという人はそもそも自分でWEBサーバなん て作ったりしませんし、(apacheを使うだろうし)ということは別に実用的なも のじゃなくても勉強になれば全然OKじゃないか。という考えです(笑) さて次は Proxy(串というやつです) でも作ってみようかなとか考えておりま す。Cで書いたらCGI Proxy としても使えそうだし。我ながらちょっと楽しみ です。ではでは。 End. written by kenji aiko 2003/11/28 Copyright (C) 2003 kenji aiko All Rights Reserved