アクセスカウンタ

アクセスカウンタをCGI、C言語で作成していきます。

アクセスが集中した場合にファイル出力が衝突し、カウント値がリセットされる現象が発生することがあります。
第5章にて、あてにならない内容ですが仮の処置を行いました。
それ以前の章では、処置を適用していません。ご了承のうえ、閲覧をお願いします。

1. ファイル操作と文字出力

実行ファイル
ソースファイル


カウント数をファイルによって読み書きします。
cgi実行ファイルには755、記録ファイルには666(読み書き可)の権限を与える必要があります。

sudo chmod 755 counter.cgi (実行ファイル)
sudo chmod 666 counter.txt (記録ファイル)

MINEヘッダ:"Content-type: text/plain;\n\n"

を始めに標準出力。
text/plainはhtmlタグなしのテキスト形式です。
数字を画像で装飾したカウンタもあるけど、いまのところは文字だけでいいかなと思う。
ファイル入出力によって、

(1)カウント数を読み込む。
(2)カウント数をインクリメント(+1)。
(3)カウント数を書き込む。
(4)カウント数を標準出力。

これで、最小限のカウンタを実装できます。
記録するファイルが存在しないときのエラー処理は実装していません。

Internal Server Errorが出る場合

プログラムが途中で停止して、MINEヘッダなどを出力しないまま終了するとでることがある。
記録するファイルが存在するか、読み書き可能か確認したら動作した。
あと、MINEヘッダは2回改行する必要があるみたい。(;\n\n)


2. 環境変数取得とログ生成

(標準出力は同じため、実行ファイルは省略)
ソースファイル

ログの例


cgi実行時には、アクセス元の情報が環境変数に格納されています。
この情報を記録することで、"どのような人が、どんな端末で、どこからアクセスしたか"を解析できます。
(まえに別のホームページをやってたときも、アクセス解析というツールがありましたなあ…)
同時に時間も記録することにします。

stdlib.hをインクルードし、getenv("環境変数名")で環境変数を取得します。
返り値は文字列です。
指定する環境変数名は、次のようなものがあります。
(私のだいたいの解釈です、本来の意味とは違うかもしれないので注意)

返す情報がない場合は、NULLが帰ってきます。
アクセス時間の情報は、cgi実行時のサーバー内部の時間を取得することにしました。
system関数とdateコマンドで、ログに追記します。

system("date >> log.txt")

しかし、時間の追記と環境変数の追記が分離していて、オーバーヘッドが大きいかもしれません。
C言語のtime.hで時間を取得すれば、一度の追記のみにできます。


3.無視リストの導入

ソースファイル

無視リストignore.txtの例


2章で取得した環境変数を使って、カウントを無視するipアドレスを登録します。
これは、主に自分が使っている端末を登録して、編集中や更新確認での無駄なカウントを防ぐためです。
アクセスカウントの大半が自分でのアクセスだったりすると…、寂しいですよね。

新たにignore.txtを作成して、無視するipアドレスを登録します。
無視リストは、外部から表示できないように注意してください!
私が自分のホームページにアクセスするときは、LANでのローカルipアドレス192.168.0.xxxでした。
また,モバイル回線を介してiPhoneからアクセスすると、グローバルipアドレスになりました。

cgiプログラムではignore.txtを読み込み、ファイルの終端までipアドレスを照らしあわせていきます。
全てのipアドレスと一致しなかったらカウントアップしてログ追記、どれかと一致したら無視、
という流れです。


4.正規表現の対応

ソースファイル


3章で作成したカウント無視機能を、正規表現(ワイルドカード)に対応させました。
ひとつの章として書くほどの内容ではありませんが…
カウントを無視するipアドレスに正規表現を用いることで、包括指定をすることができるようになります。
例えば、ローカルipアドレス192.168.0.1,2,3,…を全て書くのは効率が悪いですよね。
正規表現を含んだ文字列の比較を、regex.hを用いて次の手順で実装します。


(1)正規表現を含んだ文字列をコンパイルして、regex_t型に格納する。
(2)生成したregex_t型と、アクセス元のipアドレスを比較する。
(3)regex_t型のメモリ解放。


これを、無視リストの全てのipアドレスに対して実行していきます。
また、比較にはregexec()を使用します。
これは文字列の”検索”を行う関数ですが、一致が検出されたかを戻り値によって判定することにします。

int regexec(const regex_t *preg, const char *string, size_t nmatch,regmatch_t pmatch[], int eflags);
引用:”Man page of REGEX”,https://linuxjm.osdn.jp/html/LDP_man-pages/man3/regex.3.html,2013年02月11日

第1引数pregに、生成したregex_t型のアドレスを渡します。
第2引数に、比較する文字列(=ipアドレス)を渡します。
また第3,第4引数には検索結果の格納先を指定するのですが、ここでは格納先サイズ0、格納先アドレスNULLを指定しました。
仕様書には書かれていないコーディングですが、特にエラーは生じていません。(自己責任ということで…)

正規表現に対応すると、サーバーへの負荷が大きくなると思います。
そこで、最後にサーバーの応答時間を計測してみました。

ignore.txt:192.168.0.*
正規表現対応前:870ms
正規表現対応後:849ms

あまり変化はないみたい?
しかし、指定ipアドレスが1パターンのみなので、変化が現れていないだけかもしれません。
さらにipアドレスを追加していくと、応答速度の差が大きく出てくるかもしれません。


5.ファイル破損の防止

ソースファイル


アクセスが集中したときにカウント値がリセットされる不具合が見つかりました。
以下のコマンドによってアクセスカウンタに集中アクセスしてみます。

while :; do curl http://localhost/cgi-bin/counter.cgi;echo ""; done
※DNSサーバーへの負荷とトラフィックを考慮し、localhostで指定。

端末を複数(ここでは2つ)立ち上げ、このコマンドを同時に実行します。
すると…

ファイル出力衝突によるファイル破損

カウントリセット、出ました。
これは、複数のスレッドによってファイル出力が衝突し、ファイルが破損したために生じるようです。
そこで、ファイルの排他制御(ファイルロック)を行います。
これによって、1つのファイルを複数のプロセスで同時にアクセスすることを防ぎます。
ファイル入出力に関わる関数fopen(),scanf(),fprintf()のエラー処理を追加し、ファイル破損の原因を特定します。

3865
カウント値読込エラー
3867

結果では、fscanf()でエラーが出ました。
どうやら、カウント値読込に失敗して0が代入され、この値を+1して書込をしたためリセットされていたようです。
今回はエラーを検出しだいプロセスを終了しているため、カウント値がリセットされる現象は解消されました。
しかし、正しく排他制御をできたかというと…、排他制御にスキマがあるのかな?
それとも、また別の原因なのか…。