***HPクリエイターのためのCGI講座 第42回***
3.3 実用的なカウンター

では、実用的なカウンターを作成しましょう
このカウンターの設計方針は以下の2点です。

1.ケタ制限をなくす
前項のカウンターでは4ケタカウンターを表示する方法を学びました。
ここでは、ケタ制限をなくし、より汎用的なカウンターを考えます。
2.ファイルへの同時書込問題の解決
ファイルからの読み出しでは、問題にならないのですが、ファイルへの書込みが発生する場合、注意すべき重要な事柄があります。
ファイルへの同時書込みの問題です。

<ファイルへの同時書込み>
HPは不特定多数の人が、不定期にアクセスします。
全く同時に、同じHPにアクセスされることも想定しなければなりません。
もし、同時に複数の人から、HPをアクセスされたら、どのような問題が発生するのでしょうか?

鈴木さんと、田中さんが同時に、同じHPへアクセスした場合を考えてみましょう。
カウンター値は、ファイルに格納してありましたね。
今、鈴木さんが、HPを開いたとします。
その時CGIプログラムは、カウンター値をファイルから読み出し1upして、カウンター値をファイルへ書き込もうとします。
カウンター値を書き込む前にファイルを書込みモードでオープンしますね。
具体的には、次の命令を実行します。
open(OUT,">esy_cnt.dat");

この時に、田中さんが、同じHPのページを開いたとします。
CGIプログラムはカウンター値をファイルから読み出そうとしますが、開いたカウンター値のファイルには、中身がありません。
(鈴木さんが起動させたCGIプログラムが、まだカウンター値を書き込んでいない)
ファイルの中身がないので、田中さんが読み出したカウンター値は0になっています。
田中さんが起動したCGIは、カウンター値を1upして、ファイルに書き込もうとするので、カウンター値=1を書き込みます。
つまり、この時点でカウンター値は、破壊されます。
このような問題を防ぐために、ファイルへの書き込み時は、他の人からのファイル・アクセスを禁止する必要があります。

以上、2点を考慮したカウンターを作成してあるので、「実用的なカウンター」をクリックしてください。
最上部に複数ケタタのカウンターを、黄色の枠に、このカウンターを表示しているCGIプログラムを表示しています。
カウンター値には、巨大な値をあらかじめ設定してあります。

HPのHTMLソースコードをご覧ください。
下記の記述が、ありますね。
<P><IMG src="cnt.cgi"></P>
この記述にから、このページが開かれる度に、cnt.cgiが起動されます。
タグの書き方は、今までのカウンターと同じです。

では、cnt.cgi について、説明しましょう。

require 'gifcat.pl';
とほほさんが作成したサブルーチンの格納されているファイルを宣言 。

$lock_file = "edperl13_3.lock";
書き込み衝突問題を回避するためのファイル名。
使い方は、プログラム内で説明します。

&lock;
書き込みの衝突を回避するためにファイル・ロック用サブルーチンをコール

open(IN,"cnt.dat") || &error(1);
前項と同じように、カウンター値用ファイル(cnt.dat)を読み込みモードでオープンします。
error サブルーチンにはパラメータを渡しています。
このように、サブルーチンにパラメータを渡すときは、カッコで囲んで、渡したい値を記
述します。

$cnt = <IN>;
カウンタ値の読込み。

close(IN);
ファイルのクローズ。

$cnt++;
カウンタ値を1UP。

open(OUT,">cnt.dat") || &error(1);
cnt.datを書込みオープン。

print OUT $cnt;
カウンタ値の書込み。

close(OUT);
ファイルのクローズ。

unlink($lock_file);
ロックを解除

while(0 != $cnt){
$num_gif = $cnt % 10; # 各ケタの値を計算
unshift(@files, "c1_$num_gif.gif");# 各ケタのgifファイル名を配列に設定
$cnt = int($cnt / 10);
}
各ケタのgifファイルを配列 @file へ順次格納

上記アルゴリズムの解説
$cnt=208 だった場合を例にすると
1周目の
ループ
while(0 != $cnt){ 最初のループでは $cnt=208 なので判定は真、 ループの中へ
    $num_gif = $cnt % 10; 208 % 10 は 8
$num_gifには 8 が設定される
    unshift(@files, "c1_$num_gif.gif"); 'c1_8.gif'を配列 @files に設定
$files[0] は 'c1_8.gif'
    $cnt = int($cnt / 10); $cnt / 10 は 20.8なので$cnt は 20 になる
}
2周目の
ループ
while(0 != $cnt){ $cnt は 20 なのでループの中へ
    $num_gif = $cnt % 10; 20 % 10 は 0
$num_gifには 0 が設定される
    unshift(@files, "c1_$num_gif.gif"); 'c1_0.gif'を配列 @files に設定
$files[0] は 'c1_0.gif'
$files[1] は 'c1_8.gif'
    $cnt = int($cnt / 10); $cnt は 2 になる
}
3周目の
ループ
while(0 != $cnt){ $cnt は 2 なのでループの中へ
    $num_gif = $cnt % 10; 2 % 10 は 2
$num_gifには 2 が設定される
    unshift(@files, "c1_$num_gif.gif"); 'c1_2.gif'を配列 @files に設定
$files[0] は 'c1_2.gif'
$files[1] は 'c1_0.gif'
$files[2] は 'c1_8.gif'
    $cnt = int($cnt / 10); $cnt は 0 になる
}
4周目の
ループ
while(0 != $cnt){ $cnt は 0 なのでループの外へ
    :
}
ループから抜けると 、配列@filesには次のように設定される
    $files[0] には 'c1_2.gif'
    $files[1] には 'c1_0.gif'
    $files[2] には 'c1_8.gif'


print "Content-type: image/gif\n\n";
gifデータを送ることをブラウザへ通知

print &gifcat'gifcat(@files);
gif ファイル名を gifcat'gifcat へ渡す。
これは、次のように記述したものと同じ
print &gifcat'gifcat($files[0],$files[1],$files[2],・・・);

exit;
ここでプログラム終了。
必要はありませんが、プログラムを見やすくするために、記述しています。

sub lock {
    $retry = 5; # ロック解除リトライ回数
    while (!symlink(".", $lock_file)){
        if (--$retry <= 0) { &error(0); }
        sleep(1);# 1秒待つ
    }
}
書き込み衝突のための、ロジックです。
カウンターファイルにカウンター値を書き込む前に、$lock_file にシンボリックリンクを貼ります。
もし、シンボリックリンクにすれば、現在、他のユーザーがカウンターフィルへ書き込み
中であると判断し、1秒待って、再度シンボリックリンクにトライします。
これを5回繰り返しても、シンボリックリンクを貼れなければ、エラーとします。
この手法はunix系のサーバーにのみ、使用できます(WinNT等には使えません)。

シンボリックリンク
unixではファイル名だけ用意して、実態は別のファイル(ディレクトリ)に置くことができます。
これをシンボリックリンクと言います。
このシンボリックリンクを実現する組み込み関数が、Perlでは symlink です。
例では、自分自身のディレクトリにリンクを張っています。
(パラメータに "." が指定されている)
ここでは、どこにリンクを貼るかは重要ではなく、リンクを貼ることが可能かどうかが、重要になります。
symlink は、既にリンクが貼られていると、2重にリンクを貼れないので、失敗します(偽を返す)。
この結果を元に、他のユーザーが、現在カウンターファイルへ書き込み中かどうかを判定します。
(もし他のユーザーが先に書き込み中だと、そのユーザーが先にこの関数を呼び出しているので、既にリンクが貼られているはず)

sleep 関数は、処理を秒単位で中断します。
この例では、1秒間中断した後(パラメータが 1)プログラムを再開します。


sub error {
エラー処理用のサブルーチンです。

    if ((-e $lock_file) && ($_[0] !=0)){ unlink($lock_file); }
パラメータは配列 @_ に格納されています。
$lock_file が存在し(シンボリックリンクが貼られているとファイルは存在すると
みなされる)、パラメータが 0 でない場合、シンボリックリンクをはずす。

    @err_msg = (
    '47','49','46','38','39','61','3b','00','0f','00','80','00','00','fe','74','97',
    'ff','ff','ff','21','f9','04','01','00','00','01','00','2c','00','00','00','00',
    '3b','00','0f','00','00','08','92','00','01','08','1c','48','90','60','80','83',
    '08','13','2a','5c','c8','b0','a1','c3','82','10','07','3a','9c','48','b1','22',
    'c2','88','11','2d','6a','dc','78','51','22','c7','8f','20','3b','0a','0c','49',
    '92','a3','c1','8a','1e','0d','a6','f4','78','10','a2','42','96','18','33','8a',
    '74','19','b3','25','c6','84','12','63','de','9c','99','33','40','46','00','3e',
    '61','0a','2d','18','54','27','d0','99','0b','4f','22','c5','b9','72','e8','51',
    '8a','2c','99','3e','5d','2a','d5','e6','48','aa','13','a3','8a','4c','7a','f5',
    'e5','55','ad','4a','b3','76','ad','ea','75','ac','d5','b3','52','a7','3e','34',
    '8b','b6','ec','d4','93','60','8d','76','d5','da','d6','ad','cb','ba','45','8d',
    '6e','e5','aa','36','6d','5c','b5','01','01','00','3b');

    Error のgifパターンです。
    エラー表示に gif ファイルを使わないのは、エラーの発生した原因がわからず、原因として、ファイルシステムの異常も考えられるためです。
    つまり、エラー表示用の gif ファイルを用意していても、読み出せない可能性があるからです。

    print "Content-type: image/gif\n\n";
    foreach (@err_msg) {
        $data = pack('C*',hex($_));
        Error のgifパターンをブラウザへ送っています。

        print $data;
    }
    exit;
}

エラー処理を呼び出す個所を見直してください。
&error(0) と &error(1) の2つの形がありますね。
カッコで囲まれた数字の 0 と 1 は &error へのパラメータです。
このパラメータは、シンボリックリンクを解除するかしないかのスイッチに使います。

最初に使われている行は、次のとおりです。
open(OUT,">cnt.dat") || &error(1);
この行は、シンボリックリンクを貼った後に実行されますね。
カウンターファイルへのアクセスエラーが発生した場合に、このまま終了するとシンボ
リックリンクは残ったままになります。
そこで、error 関数へのパラメータに 1 を渡して、シンボリックリンクの解除を指示し
ています。

次に使われている個所は、lock 関数の次の行です。
if (--$retry <= 0) { &error(0); }
ここは、既に他のユーザーが、シンボリックリンクを貼っている時のエラー処理でしたね。
他のユーザによって、シンボリックリンクが貼られているのに、勝手にリンクを外す訳にはいきません。
error 関数に 0 のパラメータを渡すことで、リンクを外さないように指示しています。


********************************************************************************
講師:ALK alk@arkland.co.jp
運営:アークランド(株) http://www.arkland.co.jp/