前へ << 掲示板を作ろう (1) | CGI プログラムのはじめの一歩 >> 次へ |
一応掲示板らしき形にはなってきましたが、 書き込んだデータをどこにも保存していないので、 後から読むことができません。 CGI プログラムは、WWW 経由でアクセスがあったときだけ実行されます。 次回の起動時に書き込んだデータを読めるように、 書き込みをファイルに保存しておかなくてはいけません。後に詳しく述べますが、環境によっては事前の準備は要りませんが、 bbs-save-to-file.dat というファイルを事前に作成し、 パーミッションを 606 にしておかなくてはいけない場合もあります。
1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # データファイル名 8: $data_file = $script_name; 9: $data_file =~ s/\.cgi$/.dat/; 10: 11: # jcode.pl をロード 12: require 'jcode.pl'; 13: 14: # 引数解析 15: foreach ( split('&',$ENV{QUERY_STRING}) ){ 16: ($key,$value) = split('=',$_); 17: $value =~ tr/+/ /; 18: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 19: &jcode::convert(\$value,'euc'); 20: 21: $value =~ s/&/&/g; 22: $value =~ s/</</g; 23: $value =~ s/>/>/g; 24: 25: $value =~ s/\r\n|\r|\n/<BR>/g; 26: 27: if ( $key eq 'FROM' ){ 28: $from = $value; 29: } elsif ( $key eq 'MESSAGE' ){ 30: $message = $value; 31: } 32: } 33: 34: print "Content-type: text/html\n\n"; 35: 36: print <<END; 37: <HTML> 38: <HEAD><TITLE>掲示板</TITLE></HEAD> 39: <BODY BGCOLOR="#DDDDDD"> 40: <H1>掲示板</H1><HR> 41: <FORM METHOD=GET ACTION="$script_name"> 42: <TABLE BORDER=1> 43: <TR><TD>ハンドルネ―ム: 44: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 45: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 46: </TABLE> 47: <P><INPUT TYPE=submit VALUE="送信"> 48: </FORM><HR> 49: END 50: 51: # 発言ならデータファイルに追加 52: if ( $from ne '' && $message ne '' ){ 53: open(OUT,">> $data_file"); 54: print OUT "発言者: $from<P>\n"; 55: print OUT "$message<HR>\n"; 56: close(OUT); 57: } 58: 59: # データファイル内容を表示 60: open(IN,"$data_file"); 61: print <IN>; 62: close(IN); 63: 64: print <<END; 65: </BODY></HTML> 66: ENDまず、8: $data_file = $script_name; 9: $data_file =~ s/\.cgi$/.dat/;で $data_file にデータファイル名を代入します。 $script_name は bbs-save-to-file.cgi という文字列が入っているので、 $data_file は bbs-save-to-file.dat となります。52: if ( $from ne '' && $message ne '' ){ 53: open(OUT,">> $data_file"); 54: print OUT "発言者: $from<P>\n"; 55: print OUT "$message<HR>\n"; 56: close(OUT); 57: }$from と $message に値が設定されていたら発言と見なし、 $data_file に追加書き込み (>>) します。60: open(IN,"$data_file"); 61: print <IN>; 62: close(IN);そして発言・閲覧に関わらず、毎回データファイルの 内容を出力します。つまり、という流れになります。
- 発言ならデータファイルに追加書き込みし、データファイルの内容を出力
- 閲覧ならデータファイルの内容を出力
先のスクリプトでは、書き込んだ後リロードすると、 再度同じ発言が書き込まれてしまいます。 また、長い文字列を書き込もうとすると、414 Request-URI Too Large The requested URL's length exceeds the capacity limit for this server. request failed: URI too longとなってしまいます。これは、GET メソッドではデータの長さが制限されているからです。 どれだけの長さまで許されるかは OS や WWW ブラウザなどの環境に依存します。apache-1.3.9 では URL 部分も含めて 8190 バイトを越えると 414 Request-URI Too Large となります。 日本語1文字2バイト、URLエンコードして6バイトなので、 1300文字程度しか書けません。一行80文字でたった17行です。そこで POST メソッドを使いましょう。 POST メソッドには長さの制限はありません。また、UNIX では環境変数に代入できるデータの長さも制限があります。
GET メソッドは環境変数 QUERY_STRING から取得できましたが、 POST メソッドは標準入力からデータが渡されます。 GET でも POST でもデータの形式は変わりませんし、 URL エンコードされているのも同じです。
1: #!/usr/local/bin/perl 2: 3: # 自分自身のファイル名を取得 4: $script_name = $ENV{SCRIPT_NAME}; 5: $script_name =~ s|.*/([^/]+)$|$1|; 6: 7: # データファイル名 8: $data_file = $script_name; 9: $data_file =~ s/\.cgi$/.dat/; 10: 11: # jcode.pl をロード 12: require 'jcode.pl'; 13: 14: # 標準入力からデータを読み込む 15: read(STDIN,$buf,$ENV{CONTENT_LENGTH}); 16: 17: # 引数解析 18: foreach ( split('&',$buf) ){ 19: ($key,$value) = split('=',$_); 20: $value =~ tr/+/ /; 21: $value =~ s/%([0-9a-fA-F][0-9a-fA-F])/pack("C",hex($1))/eg; 22: &jcode::convert(\$value,'euc'); 23: 24: $value =~ s/&/&/g; 25: $value =~ s/</</g; 26: $value =~ s/>/>/g; 27: 28: $value =~ s/\r\n|\r|\n/<BR>/g; 29: 30: if ( $key eq 'FROM' ){ 31: $from = $value; 32: } elsif ( $key eq 'MESSAGE' ){ 33: $message = $value; 34: } 35: } 36: 37: print "Content-type: text/html\n\n"; 38: 39: print <<END; 40: <HTML> 41: <HEAD><TITLE>掲示板</TITLE></HEAD> 42: <BODY BGCOLOR="#DDDDDD"> 43: <H1>掲示板</H1><HR> 44: <FORM METHOD=POST ACTION="$script_name"> 45: <TABLE BORDER=1> 46: <TR><TD>ハンドルネ―ム: 47: <TD><INPUT TYPE=text NAME=FROM SIZE=54> 48: <TR><TD COLSPAN=2><TEXTAREA ROWS=6 COLS=60 NAME=MESSAGE></TEXTAREA> 49: </TABLE> 50: <P><INPUT TYPE=submit VALUE="送信"> 51: </FORM><HR> 52: END 53: 54: # 発言ならデータファイルに追加 55: if ( $from ne '' && $message ne '' ){ 56: open(OUT,">> $data_file"); 57: print OUT "発言者: $from<P>\n"; 58: print OUT "$message<HR>\n"; 59: close(OUT); 60: } 61: 62: # データファイル内容を表示 63: open(IN,"$data_file"); 64: print <IN>; 65: close(IN); 66: 67: print <<END; 68: </BODY></HTML> 69: END
15: read(STDIN,$buf,$ENV{CONTENT_LENGTH});まず標準入力から $ENV{CONTENT_LENGTH} の分だけデータを読み込み、 $buf に代入します。18: foreach ( split('&',$buf) ){GET メソッドの場合は $ENV{QUERY_STRING} を split していましたが、 今回は $buf を split します。44: <FORM METHOD=POST ACTION="$script_name">忘れてはいけないのが、フォームの METHOD を POST に変えておくことです。
掲示板としての基本動作には直接関係ないのですが、 掲示板にありがちな発言時刻とホスト名を表示してみましょう。 以下は変更点のみです。55: if ( $from ne '' && $message ne '' ){ 56: open(OUT,">> $data_file"); 57: print OUT "発言者: $from<P>\n"; 58: print OUT "$message<HR>\n"; 59: close(OUT); 60: } 61: 62: # データファイル内容を表示 63: open(IN,"$data_file"); 64: print <IN>; 65: close(IN); 66: 67: print <<END; 68: </BODY></HTML> 69: END簡単に言うと $now_date に現在時刻を、$host に発言者のホスト名を代入し、 それをデータファイルに書き出しているだけです。まずは発言時間から。time 関数を呼ぶと、 その時点での 1970年1月1日からの経過秒数を返します。 2000年1月1日午前9時16分で 946685762 くらいです。 localtime は 1970年1月1日からの経過秒数を 秒/分/時/日/月/年 として返します。 つまり、localtime(time()) は現在時刻を 秒/分/時/日/月/年 の形式で返すわけです。
ただし、年 ($year) は西暦から 1900 を引いた値になります。西暦 2001 年なら 101 という値が得られるので、1900 を足します ($year += 1900)。
以下は想像ですが、初期の UNIX の localtime 関数は西暦の下 2桁を返していました。 1980 なら 80 を返していたわけです。 しかしこれでは 2000 年になると 00 を返してしまいます。だからと言って 西暦をそのまま返すようにすると、これまでのプログラムが正しく動作しなくなってしまいます。 互換性と2000年問題を両方解決したいため、結果として「西暦から 1900 を引いた値」 を返すという変な動作をすることになったわけです。また、月は 1〜12 でなく 0〜11 という値が返されますので、 1を加算します ($mon++)。それらの値を
59: close(OUT); 60: }で、「2000/01/01 09:32:08」という形にして $now_date に代入します。もし sprintf を使わず$now_date = "$year/$mon/$day $hour:$min:$sec";とすると、2000/1/1 9:32:8 と、「01 月」でなく「1月」となってしまいます。 もちろん「1月」の方がよければこれでも構わないのですが、個人的には桁数が常に 同じ方が好みなので、sprintf を使っています。次に発言者のホスト名です。基本的には
が WWW サーバにより代入されることになっています。
- 環境変数 REMOTE_HOST には X68000.q-e-d.net のようなホスト名
- 環境変数 REMOTE_ADDR には 133.8.3.1 のような IP アドレス
ただし、最近の apache の初期設定では REMOTE_ADDR には IP アドレスが入っていますが、 REMOTE_HOST には何も値が入っていません。 これはあくまでも初期設定の話なので、管理者が設定変更していたら REMOTE_HOST にホスト名が入っている場合もあります。
この理由を説明すると細かい話になるのですが、ブラウザを使って web を見るということは、 ブラウザの動いているマシンから WWW サーバのマシンへ向かって TCP/IP コネクションを張るということです。 このとき WWW サーバからは、相手側のマシン (ブラウザの動いているマシン) の IP アドレスを必ず知ることができます。なぜなら TCP/IP パケットのヘッダには 送信元の IP アドレスが記録されているからです。 その IP アドレスを環境変数 REMOTE_ADDR に設定してから CGI プログラムを実行するわけです。WWW サーバの設定によりしかし相手側のホスト名は TCP/IP パケットには記録されていません。 そのため、ホスト名を知りたければ いちいち DNS サーバに問い合わせて、IP アドレスから ホスト名に変換しないといけないのです (これを「逆引き」と言います)。 つまり、web を見るたびに WWW サーバは毎回 DNS サーバと通信して逆引きを 依頼しなくてはいけません。結果的にネットワークに負荷をかけ、 レスポンスの低下を招きます。それを嫌って apache のデフォルトは 「逆引きをしない」設定になっています。 これはあくまでもデフォルト設定なので、httpd.conf の HostnameLookups Off を HostnameLookups On に変えれば逆引きしてくれるようになります。
掲示板を読むときは、やはり新しい発言から順に表示された方が便利です。 しかし現在の掲示板は、ファイルに追加書き込みしているため、 最新の発言が一番下に表示されてしまいます。 そこで、最新の発言をファイルの先頭に記録するようにしましょう。69: ENDまずデータファイルから全データを @buf に代入します。 これまではデータファイルに追加 (>>) していましたが、 今度は先頭に (>) 最新情報を書き込みます。 そしてその後に @buf を再度書き込みます。ただし、この書き方ではファイルの内容が全て @buf に代入されます。 もしデータファイルが 1MB あれば、メモリも 1MB (+α) 必要になり、 マシンに負荷がかかります。 これが気になる場合は
open(TMP_OUT,"> tmp_file"); print TMP_OUT "発言者: $from<BR>\n"; print TMP_OUT "$now_date $host<BR>\n"; print TMP_OUT "$message<HR>\n"; open(IN,"$data_file"); while (<IN>){ print TMP_OUT $_; } close(IN); close(TMP_OUT); # tmp_file の内容を $data_file にコピーするだけ open(TMP_IN,"tmp_file"); open(OUT,"> $data_file"); while (<TMP_IN>){ print OUT $_; } close(TMP_IN); close(OUT);と、まずテンポラリファイル (tmp_file) に書き込んで、 それを元のファイル ($data_file) にコピーすればよいでしょう。
前へ << 掲示板を作ろう (1) | CGI プログラムのはじめの一歩 >> 次へ |