| counter.cgi | 2002-06-11(Tue) 08:50 |
M.Kamadaの自作CGIカウンタのPerlのソースリストを公開します。この自作CGIカウンタは、STUDIO KAMADAの各ページの右下のカウンタとして実際に使用しているものです。
- 特徴と仕様
このCGIカウンタの最大の特徴は、カウンタの自己修復機能を持っていることです。このカウンタはもともと壊れる確率がじゅうぶん小さくなるように作られています(一般的なプロバイダを利用している限り、自作CGIカウンタを絶対に壊れないようにすることは不可能です)。それでも万が一壊れてしまった場合には、バックアップを利用して高い確率で自己修復します。さらに、自己修復に失敗した場合に備えて、手動で正しいカウントに近い値に戻すことができるようにカウンタの値のバックアップを数多く残します。例えば、毎日アクセスされるページならば、1日の中で最後にアクセスされたときのカウンタの値が1ヶ月前の分まで保存されます。
このCGIカウンタは、ページビュー数ではなく、ビジット数を数えます。すなわち、同じ人が短い間隔で繰り返し訪れた場合(ブラウザで再読み込みを行った場合など)は最初の1回だけを数えます。そのために訪問者のIPアドレスを一時的に保存しますが、そのログファイルをハッシュ関数を使って64個に分散させています。これは、時間のかかるログファイルの更新のためのロックが衝突して待たされる確率を小さくするための工夫です。同じ人がカウンタを更新できる間隔は10分に設定されています。10分以内に同じIPアドレスからアクセスがあった場合はカウンタを更新しません。10分以上前に記録されたIPアドレスは、次に同じログファイルがアクセスされたときに自動的に消去されます。
このCGIカウンタは、複数のページに別々のカウンタを設置することができます。ただし、名前はCGIファイルの中で固定します。カウンタを増やす場合はCGIを書き換える必要があります。
ファイルのロックにはmkdirを使用し、IPアドレスのログとカウンタデータのファイルを更新するときは新しいファイルを作っておいてからrenameで古いファイルと置き換えています。これは、ファイルの更新中にアボートしてしまった場合にファイルが壊れる確率を減らすためです。
数字のGIF画像の連結に杜甫々(とほほ)さんの「gifcat.pl: GIFファイル連結ライブラリ Ver1.61」を使用します。便利なライブラリを作成・公開された杜甫々(とほほ)さんに感謝いたします。このライブラリは、「とほほのWWW入門」で入手することができます。
- 動作環境
このCGIはPerl 5を前提としています。
動作確認は@niftyの@homepageで行っています(実際に使用しています)。
!!!注意!!!
M.Kamadaが利用している@niftyの@homepageでは、自作CGIを置くディレクトリがもともと外部からアクセスできないようになっています。そのため、このCGIではファイルのパーミッションについてほとんど考慮していません。@homepage以外で利用される場合は、各自でファイルのパーミッションを調整してください。もしかしたらCGIの書き換えが必要かも知れません。
- ファイルの一覧
counter.cgi … このCGIカウンタの本体
gifcat.pl … 杜甫々さんのGIF画像連結ライブラリ
fig/?.gif … 0〜9の数字の画像(?=0〜9)
[名前]/ … 個々のカウンタのためのディレクトリ(名前は自由に設定できます)
[名前]/counter.dat … カウンタデータ(現在のカウンタの値が入っています)
[名前]/counter.loc … カウンタデータのロック(ロックされている間だけ存在します)
[名前]/counter.new … カウンタデータのテンポラリ(更新している間だけ存在します)
[名前]/counter.d?? … 日のバックアップ(??=01〜31)
[名前]/counter.h?? … 時のバックアップ(??=00〜23)
[名前]/counter.s?? … 秒のバックアップ(??=00〜59)
[名前]/log??.txt … IPアドレスと前回のアクセス時刻が記録されるログファイル(???=0〜63)
[名前]/log??.loc … IPアドレスのログファイルのロック(???=0〜63、ロックされている間だけ存在します)
[名前]/log??.new … IPアドレスのログファイルのテンポラリ(???=0〜63、更新している間だけ存在します)
- 使い方
プロバイダ毎にCGIの使い方が異なりますので、一般的なことはプロバイダの説明に従ってください。
CGIを置くディレクトリを確認してください。このディレクトリをここではcgi-binディレクトリと呼ぶことにします。プロバイダの仕様に合わせて読み換えてください。
カウンタの名前を決めてください。例えば、日記ならば「diary」でよいでしょう。
counter.cgiの最初のほうにある@dirsという配列の要素がカウンタの名前です。このCGIカウンタでは、@dirsに書いてある名前以外の名前は受け付けないようになっています。あらかじめ決めておいた名前をここに書いてください。
counter.cgiとgifcat.plをcgi-binディレクトリにアップロードしてください。counter.cgiには実行属性を付けてください。
cgi-binディレクトリに「fig」というサブディレクトリを掘り、その中に0.gif〜9.gifというファイル名の数字の画像のGIFファイルをアップロードしてください。下の画像を保存してそのまま使っても構いません。
cgi-binディレクトリにカウンタの名前のサブディレクトリを掘ってください。中のファイルは自動的に生成されますので空のままで大丈夫です。
HTMLファイルから次のようにしてこのCGIカウンタを呼び出します。
<IMG SRC="[このCGIのURL]?[名前]" ALT="counter">
- カウンタの自己修復機能について
このCGIカウンタはもともと壊れる確率がじゅうぶん小さくなるように作られていますが、それでも多数のアクセスが重なった場合に壊れてしまう確率が0ではありません。そこで、万が一カウンタデータが壊れた場合に備えて、このCGIカウンタには自己修復機能を持たせてあります。
このCGIカウンタは、カウンタの値をカウンタデータ(counter.dat)へ出力した直後に、3つのバックアップにカウンタの値を出力します。それは「日のバックアップ」(counter.d??)、「時のバックアップ」(counter.h??)、「秒のバックアップ」(counter.s??)の3つです。「日のバックアップ」と「時のバックアップ」は、万が一自己修復機能が正しく機能しなかった場合に手動で復旧するためのものです。自己修復機能では「秒のバックアップ」を使用します。
自己修復機能の仕掛けは以下のようになっています。
- まず、他のCGIカウンタと同様に、カウンタをロックしてからカウンタデータから現在のカウンタの値を読み出します。このとき読み出せなければ、とりあえず現在のカウンタの値は0とみなします。
- 次に、「秒のバックアップ」をすべて読み込み、その中で一番大きな値をバックアップデータとします。
- 現在のカウンタの値がバックアップデータよりも小さい場合と、現在のカウンタの値がバックアップデータよりも1000以上大きい場合は、カウンタデータが壊れていると判断します。
- カウンタデータが壊れている場合は、カウンタデータから読み出した値を無視して、バックアップデータを現在のカウンタの値として利用し、カウンタデータを復元します。
- 前回のアクセスから10分以上経過している場合はカウントアップします。
- カウントアップしたときと、カウンタを復元したときは、カウンタの値をカウンタデータに出力します。
- カウンタを復元しなかった場合だけ、カウンタの値を「日のバックアップ」と「時のバックアップ」と「秒のバックアップ」に出力します。「日のバックアップ」の拡張子の末尾の2文字は現在の日付の日、「時のバックアップ」の拡張子の末尾の2文字は現在の時刻の時、「秒のバックアップ」の拡張子の末尾の2文字は時刻の秒になります。復元しなかった場合だけバックアップするのは、復元作業に失敗した場合に間違ったデータをバックアップに残さないためです。
- カウンタのロックを解除します。
- 自己修復に失敗した場合の手動での修復の仕方
まずあり得ないと思いますが、サーバのトラブルなどで「秒のバックアップ」が正しい値よりも大きくなりすぎると、自己修復に失敗します。万が一自己修復に失敗してカウンタが壊れてしまったときは、次の手順で修復してください。
- まず最初に、壊れたカウンタの動作を止めてください。CGIのファイル名を変えてしまうのが簡単ですが、壊れていないカウンタも表示されなくなってしまうので、CGIの先頭の名前のリストから壊れたカウンタの名前を削除してもよいでしょう。
- 壊れたカウンタのディレクトリにある「秒のバックアップ」(counter.s??)をすべて削除してください。リスタートしたときに秒のバックアップが残っていると自己修復機能が働いて再び壊れてしまいます。「秒のバックアップ」が1つもなければ自己修復機能は働きません。
- 「時のバックアップ」(counter.h??)と「日のバックアップ」(counter.d??)を併せて新しい順に参照して、壊れているファイル(ファイルサイズが0だったり、内容がカウンタの値として正しくないもの)をすべて削除し、壊れていない一番新しいファイルをカウンタデータ(counter.dat)へコピーしてください。
- CGIのファイル名を戻す(またはCGIの先頭の名前のリストに壊れたカウンタの名前を加える)ことで、壊れたカウンタをリスタートさせてください。カウンタを使用しているページを再読み込みしてカウンタの値が正しい値に近い値に戻っていれば完了です。
- お約束
この自作CGIカウンタの動作は一切保証しません。また、この自作CGIカウンタの内容を予告なく変更する場合があります。
- ダウンロード
counter.cgiを入手するには、下からコピー&ペーストするか、counter.cgiをダウンロードしてファイル名を「counter.cgi」に変更してください。
「gifcat.pl」は、杜甫々(とほほ)さんの「とほほのWWW入門」から最新版をダウンロードしてください。
CGIの最終更新日: 2001年5月15日
#!/usr/local/bin/perl
#許可する名前
@dirs = ("diary", "docsx68k", "x68ksoft", "docsproc", "docsmath", "puzzgame", "fractal", "downcgi");
#名前を確認する
$cntdir = $ARGV[0];
$err = 1;
for (@dirs) {
if ("$cntdir" eq $_) {
$err = 0;
last;
}
}
if ($err) {
exit(1);
}
#カウンタの桁数(0のときはゼロサプレスする)
$cntlen = 0;
#カウンタが1度にどの程度大きくなっていたら壊れたと判断するか
$cntbro = 1000;
#数字のGIFファイルのディレクトリ
$figdir = "fig";
#ロックファイルをいつまで残すか(秒数)
$locspn = 60*5;
#IPアドレス履歴ファイルをいつまで残すか(秒数)
#IPアドレス履歴ファイルは毎回コピーするのであまり長くしないこと
$adrspn = 60*10;
#今回の時刻を得る
$curtim = time();
@days = ("日","月","火","水","木","金","土");
($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = localtime($curtim);
$curdate = sprintf("%04d-%02d-%02d(%s)", 1900+$year, 1+$mon, $mday, @days[$wday]);
$curtime = sprintf("%02d:%02d:%02d", $hour, $min, $sec);
#IPアドレス履歴ファイルをロックできなかったときは無条件にカウントアップする
$inccnt = 1;
#今回のIPアドレスを得る
$curadr = $ENV{'REMOTE_ADDR'};
if (length("$curadr") != 0) {
#IPアドレスが得られたとき
#IPアドレスハッシュ値
($ip1, $ip2, $ip3, $ip4) = split(/\./, "$curadr");
$iphash = ($ip1*257 + $ip2*263 + $ip3*269 + $ip4*271) & 0x3f;
#IPアドレス履歴ファイル名
$adrlog = "$cntdir/log$iphash.txt";
$adrnew = "$cntdir/log$iphash.new";
$adrloc = "$cntdir/log$iphash.loc";
$adrlcd = 0;
#アボートしてもロックファイルが残らないようにする
#他のスレッドが作ったロックファイルを消さないようにするため$adrlcdをチェックする
#mkdirしてから$adrlcdをセットするまでの間にアボートするとロックファイルを消し損ねてしまう
#クリティカルな時間をこれより短くすることは可能か?
#ロックファイルを消す前に自分が作ったロックファイルかどうかを調べる術があればよいのだが…
sub sigadr {
if ($adrlcd) {
unlink("$adrnew");
rmdir("$adrloc");
}
exit(0);
}
$SIG{'PIPE'} = $SIG{'INT'} = $SIG{'HUP'} = $SIG{'QUIT'} = $SIG{'TERM'} = "sigadr";
#IPアドレス履歴ファイルをロックする
unless ($adrlcd = mkdir("$adrloc", 0755)) {
#ロック失敗
#古いロックファイルを削除する
#ロックファイルの日付を調べてからそれを削除するまでの間に
#他のスレッドがロックファイルの削除と作成を行っていると
#それを誤って削除してしまうことになる
if (stat("$adrloc")) {
if ($_[9] < $curtim-$locspn) {
rmdir("$adrloc");
}
}
for (1..5) {
#1秒待つ
sleep(1);
if ($adrlcd = mkdir("$adrloc", 0755)) {
#ロック成功
last;
}
}
}
if ($adrlcd) {
#IPアドレス履歴ファイルをロックできたとき
#新しいIPアドレス履歴ファイルを作る
#IPアドレス履歴ファイルの後ろのほうが古い履歴になる
if (open(OUT, "> $adrnew")) {
printf(OUT "%010d %s %s %s\n", $curtim, $curdate, $curtime, $curadr);
if (open(IN, "$adrlog")) {
while (<IN>) {
#前回の時刻(秒数),前回の日付,前回の時刻,アドレス
($logtim, $logdate, $logtime, $logadr) = split(/ /, $_, 4);
$logadr = substr($logadr, 0, length($logadr)-1);
#古いログは無視する
if ($logtim < $curtim-$adrspn) {
last;
}
if ($logadr eq $curadr) {
#最近同じアドレスからアクセスされた
$inccnt = 0;
} else {
#アドレスが違うのでそのままコピーする
printf(OUT "%010d %s %s %s\n", $logtim, $logdate, $logtime, $logadr);
}
}
close(IN);
}
close(OUT);
#IPアドレス履歴ファイルを入れかえる
rename("$adrnew", "$adrlog");
}
#IPアドレス履歴ファイルのロックを解除する
rmdir("$adrloc");
}
}
#カウンタ関連ファイル名
$cntdat = "$cntdir/counter.dat";
$cntnew = "$cntdir/counter.new";
$cntloc = "$cntdir/counter.loc";
$cntdxx = sprintf("%s/counter.d%02d", $cntdir, $mday);
$cnthxx = sprintf("%s/counter.h%02d", $cntdir, $hour);
$cntsxx = sprintf("%s/counter.s%02d", $cntdir, $sec);
$cntlcd = 0;
#アボートしてもロックファイルが残らないようにする
#他のスレッドが作ったロックファイルを消さないようにするため$cntlcdをチェックする
#mkdirしてから$cntlcdをセットするまでの間にアボートするとロックファイルを消し損ねてしまう
#クリティカルな時間をこれより短くすることは可能か?
#ロックファイルを消す前に自分が作ったロックファイルかどうかを調べる術があればよいのだが…
sub sigcnt {
if ($cntlcd) {
rmdir("$cntloc");
}
exit(0);
}
$SIG{'PIPE'} = $SIG{'INT'} = $SIG{'HUP'} = $SIG{'QUIT'} = $SIG{'TERM'} = "sigcnt";
#カウンタファイルをロックする
unless ($cntlcd = mkdir("$cntloc", 0755)) {
#ロック失敗
#古いロックファイルを削除する
#ロックファイルの日付を調べてからそれを削除するまでの間に
#他のスレッドがロックファイルの削除と作成を行っていると
#それを誤って削除してしまうことになる
if (stat("$cntloc")) {
if ($_[9] < $curtim-$locspn) {
rmdir("$cntloc");
}
}
for (1..5) {
#1秒待つ
sleep(1);
if ($cntlcd = mkdir("$cntloc", 0755)) {
#ロック成功
last;
}
}
}
if ($cntlcd) {
#カウンタファイルをロックできたとき
#カウンタを読み込む
if (open(IN, "$cntdat")) {
$count = <IN>;
close(IN);
} else {
$count = 0;
}
#カウンタが壊れていたら修復を試みる
$broken = 0;
$k = 0;
for ($i = 0; $i < 60; $i++) {
#バックアップの中で一番大きなカウンタを探す
if (open(IN, sprintf("%s/counter.s%02d", $cntdir, $i))) {
$j = <IN>;
close(IN);
if ($j > $k) {
$k = $j;
}
}
}
if ($k > 0) {
#バックアップが1つでもあるとき
if (($k > $count) || ($k + $cntbro < $count)) {
#カウンタが壊れていると思われるので修復する
$count = $k;
$broken = 1;
}
}
#最近アクセスしていなければカウントアップする
if ($inccnt) {
$count++;
}
#カウントアップしたときとカウンタが壊れていたときはカウンタを更新する
if ($inccnt || $broken) {
#カウンタを更新する
if (open(OUT, "> $cntnew")) {
print OUT "$count";
close(OUT);
}
#カウンタファイルを入れかえる
rename("$cntnew", "$cntdat");
}
#修復しなかったときだけバックアップする
if ($inccnt) {
if (open(OUT, "> $cntdxx")) {
print OUT "$count";
close(OUT);
}
if (open(OUT, "> $cnthxx")) {
print OUT "$count";
close(OUT);
}
if (open(OUT, "> $cntsxx")) {
print OUT "$count";
close(OUT);
}
}
#ロックを解除する
rmdir("$cntloc");
#カウンタを文字列に変換する
if ($cntlen) {
$cntstr = sprintf(sprintf("%%0%dld", $cntlen), $count);
} else {
$cntstr = "$count";
}
#カウンタの数字の画像を出力する
#本当はカウンタを増やしたときだけGIFファイルを作りたい
require "gifcat.pl";
for ($i = 0; $i < length($cntstr); $i++) {
$n = substr($cntstr, $i, 1);
push(@files, "$figdir/$n.gif");
}
printf "Content-type: image/gif\n";
printf "\n";
binmode(STDOUT);
print &gifcat'gifcat(@files);
} else {
#ダミーの画像を吐き出す
#サイズが1x1の透明なGIF
printf "Content-type: image/gif\n";
printf "\n";
binmode(STDOUT);
print pack("C*", (0x47, 0x49, 0x46, 0x38, 0x39, 0x61, 0x01, 0x00, 0x01, 0x00, 0x80, 0x00, 0x00, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x21, 0xf9, 0x04, 0x05, 0x14, 0x00, 0x01, 0x00, 0x2c, 0x00, 0x00, 0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x00, 0x02, 0x02, 0x4c, 0x01, 0x00, 0x3b));
}
#終わり
exit(0);