CGIプログラムの作成に役立つ技術を学ぶ


クッキーを使った値の受け渡し

これまではCGIのプログラムに値を渡すときに、URLにパラメータを付けたり、フォームを使用して値を送信してきました。 今回は、CGIのプログラムに値を受け渡す別の方法として、クッキー(Cookie)という仕組みを利用します。

クッキーとは、クライアント(Webページの閲覧者が使用しているコンピュータのこと)に、ブラウザを通して情報を保存する技術です。 ユーザーIDやパスワードなどを保存して、ユーザーを識別する(セッションを維持する)ために用いられたり、ユーザーのアクセス履歴を記録するために用いられます。

STドメインの環境では、クッキーは次のフォルダに保存されます。 (隠しフォルダになっていますが、エクスプローラのアドレスバーにパスを入力すれば開くことができます。)

H:\.NT/_Cookies        # 大学の演習室の環境の場合

下のCGIプログラムは、フォームから送信されたユーザー名とパスワードの値を、クッキーとしてユーザーのコンピュータに保存し、最後のページでそれらの値をクライアントから取得して表示します。

ユーザー情報入力ページ
(2ページ目と3ページ目の間で、パラメータやフォームを使わずに、クッキーによって 値の受け渡しをしています。)

クッキーの情報はHTTPヘッダーの一部としてブラウザへ渡されます。 これまでのCGIプログラムでは、HTML文書と一緒に次のようなHTTPヘッダーを送信 していました。

Content-type: text/html; charset=euc-jp    #この部分がHTTPのヘッダー

<html>                                     #これ以降はHTTPの本文(HTML文書)
...                                        #

ヘッダーと本文の境目は1行空ける決まりになっています。 このHTTPヘッダーに Set-Cookie: という行を付け加えることで、クライアントに クッキーを保存することができます。

Content-type: text/html; charset=euc-jp    #
Set-Cookie: user_name=chukyo;          #この部分がHTTPのヘッダー
Set-Cookie: password=taro;             #

<html>                                     #これ以降はHTTPの本文(HTML文書)
...                                        #

クライアントに保存したクッキーをCGIプログラムで取得するときは、CGIクラスを 次のように利用します。

require "cgi"

cgi = CGI.new
id = cgi.cookies["user_name"][0]  #"user_name" という名前で保存されたクッキーの値を取得。
pw = cgi.cookies["password"][0]   #"password" という名前で保存されたクッキーの値を取得。

これまでは、ユーザーのセッションを維持するためにフォームなどを使って常に ユーザー名などの値を送信し続けなければいけませんでした。しかし、クッキーを 使えば、クライアントに保存されたセッション情報を必要なときに取り出して利用 できますので、毎回毎回ユーザー名を送信する必要がなくなります。

また、ユーザーが初めてサイトに訪れたときに、サーバー側で自動的に生成した ユーザーIDなどの値をクッキーとして保存しておけば、ユーザーに意識させることなく ユーザーの識別を行うことができます。この方法なら、ユーザーがシステムにログイン しなくてもユーザーの足取りを追跡できます。オンラインショッピングサイトなど ではよく用いられる手法です。(ユーザーがリンクを辿ってサイト内のWebページを移動 するたびに、保存したユーザーIDを取得するようにしておきます。「誰が」「いつ」 「どのページを」閲覧したか常に記録し続ければ、そのユーザーの興味や趣向を判別 できますので、その情報を元にお奨め商品などを提示することができます。)

課題1-1

課題1-1-1

クッキーを使ったCGIプログラムを作成してみましょう。
下のset_cookie.cgiとget_cookie.cgiをCGIサーバーに作成し、実際にクライアントに 保存した値を取得することができているか、ブラウザから実行して確認しなさい。

set_cookie.cgi

#!/usr/bin/ruby
$KCODE = "e"

print <<EOS
Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=chukyo;
Set-Cookie: password=taro;

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
<title>クッキーを保存するCGIプログラム</title>
</head>
<body>
クッキーを保存しました。<br>
<br>
<br>
<a href="get_cookie.cgi">保存したクッキーを取得する</a><br>
</body>
</html>
EOS

get_cookie.cgi

#!/usr/bin/ruby
$KCODE = "e"

require "cgi"
cgi = CGI.new
name = cgi.cookies["user_name"][0]
pswd = cgi.cookies["password"][0]

print <<EOS
Content-type: text/html; charset=euc-jp

<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=EUC-JP">
<title>クッキーを取得するCGIプログラム</title>
</head>
<body>
クッキーを取得しました。<br>
<br>
<br>
あなたのユーザー名は #{name} です。<br>
あなたのパスワードは #{pswd} です。<br>
</body>
</html>
EOS

このクッキーの情報は、ブラウザを終了するまでの間、クライアントのコンピュータに 保存されています。一端ブラウザを閉じて、今度はget_cookies.cgiを最初に開いて みましょう。先程保存されたユーザーIDとパスワードの情報が、ブラウザを閉じたこと によって消去されたことがわかると思います。

課題1-1-2

クッキーを使ってユーザー認証を行う次のようなCGIプログラムを作成しなさい。

ユーザー情報入力ページ

[クッキーの値に日本語の文字列を指定するときは(文字列のエンコーディング)]

クッキーの値に、日本語のような2バイト文字や、半角スペース、";"、"," など の記号を含む文字列を指定するときは、次のように、文字列をエンコード(符号化) しておくと安全です。

#エンコーディングされていないHTTPヘッダー
Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=中京太郎;
Set-Cookie: password=chukyo taro;


#エンコーディングされたHTTPヘッダー
Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=%C3%E6%B5%FE%C2%C0%CF%BA;
Set-Cookie: password=chukyo+taro;

文字列をエンコードするときは、CGIクラスのescapeメソッドを使用するとよい でしょう。

require "cgi"

name = "中京太郎"
pswd = "chukyo taro"

print <<EOS
Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=#{CGI.escape(name)};
Set-Cookie: password=#{CGI.escape(pswd)};

EOS

CGIクラスのescapeメソッドは、文字列をクッキーの値として使用しても問題の ないものに変換してくれます。

Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=%C3%E6%B5%FE%C2%C0%CF%BA;
Set-Cookie: password=chukyo+taro;

[クッキーに有効期限を設定するには]

クッキーには有効期限を設定することができます。有効期限を設定すると、 ブラウザを閉じても、有効期限が過ぎるまでクッキー情報がクライアントの ハードディスク内に残り続けます。(オンラインショッピングサイトの中には、 この方法を利用してユーザーを特定し、後日そのクライアントがサイトに アクセスしたときに、これまでのアクセス履歴や購入履歴などを元に、表示する 広告の内容を変化させるなどの工夫がされているものもあります。)

クッキーに有効期限を設定するときは、クッキーのexpiresというパラ メータに、次のような形式で時刻を指定します。

Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=%C3%E6%B5%FE%C2%C0%CF%BA; expires=Fri, 25 Nov 2012 00:00:00 GMT;
Set-Cookie: password=chukyo+taro; expires=Fri, 25 Nov 2012 00:00:00 GMT;

上の例では、有効期限を2012年11月25日の午前9時に設定しています。 時刻は日本標準時ではなく、グリニッジ標準時で表すことに注意してくだ さい。CGIクラスのrfc1123_dateというメソッドを使用すると、上の ような時刻を表す文字列を、Timeオブジェクトを元に簡単に生成する ことができます。

#有効期限を1週間後に設定する場合
str = CGI::rfc1123_date(Time.now + (7 * 24 * 60 * 60))
p str
"Fri, 25 Nov 2012 00:00:00 GMT"  #時刻はグリニッジ標準時に変換されます。

[クッキーの値を削除するには]

クライアントに保存したクッキーを削除するときは、クッキーの有効 期限に過去の時刻を設定します。

#有効期限を1日前に設定してクッキーを削除する

time = CGI::rfc1123_date(Time.now - (24 * 60 * 60))

print <<EOS
Content-type: text/html; charset=euc-jp
Set-Cookie: user_name=; expires=#{time};
Set-Cookie: password=; expires=#{time};

EOS

ファイルをロックする

CGIプログラムのような、複数のユーザーが同時にシステムにアクセスすることが考えられる プログラムでは、ファイルへの読み書きを行う際に注意しなければいけないことがあります。 例えばじゃんけんゲームなどのCGIプログラムで、2人のユーザーがほぼ同じタイミングで テキストファイルへの読み書きを行ったとしましょう。その場合次のように、ファイルから 正しいアカウント情報を読み込めないケースが発生する可能性があります。

Aさんの実行過程 Bさんの実行過程
所持金の値を変更するためにplayerData.txtを開く。 アカウント認証を行うためにplayerData.txtを開く。
fo = open("playerData.txt", "w")
書き込みモードでファイルを開く。
(ファイルの中身が空になる。)
fo = open("playerData.txt", "r")
読み込みモードでファイルを開く。
(ファイルの中身は空になっている。)
fo.print ...
ファイルオブジェクトへの書き込み操作。
(実際のファイルには、まだ反映されない。)
ary = fo.readlines
ファイルからの読み込み。
(ファイルの中身は空のまま。)
fo.close
ファイルを閉じる。
(書き込みが実際のファイルに反映される。)
fo.close
ファイルを閉じる。
(アカウント情報を一つも読み込めなかった。)
変更完了。

認証失敗。
(あるはずのアカウントがみつからなかった。)

さらに、上のように中身が空になっている状態のファイルを読み込んだ後に、 読み込んだ情報をファイルに書き戻す処理を行う場合もあるかもしれません。 その場合、空の情報をファイルに書き戻すことになりますので、それまでに記録 したすべての情報が失われてしまうことになります。

このような問題を回避するために、CGIプログラムでは、一つのファイルに対して 複数のプロセス(プログラムの処理や実行過程のこと)が同時に読み書きを 行わないように、一時的にファイルにロック(鍵)をかけて、他のプロセスの 読み書きをブロックする必要があります。

ファイルのロックには、排他ロックと共有ロックの2種類があります。 排他ロックはファイルへの書き込みを行う際に使用され、共有ロックは読み込みを行う際に使用されます。 排他ロックのかかっているファイルには、他のプロセスがロックをかけることが できません。排他ロックが解除されるまで待ち続けて、解除されてから 自身のロックをかけることになります。 共有ロックのかかっているファイルの場合、他のプロセスが共有ロックをかける ことはできますが、排他ロックをかけることはできません。解除されるのを待ち、 共有ロックの解除後に排他ロックをかけることになります。

他プロセスの振る舞い
排他ロック 共有ロック ロックなし
ファイルの状態 排他ロック中 解除待ち 解除待ち
(排他ロックを無視)
共有ロック中 解除待ち
ロックされていない

排他ロックは、次のように書き込みモードでファイルを開くときに使用します。

fo = open("data.txt", "w")
fo.flock(File::LOCK_EX)    #ファイルに排他ロックをかける。
...
ファイルへの書き込みなどの処理
...
fo.flock(File::LOCK_UN)    #ロックを解除する。
fo.close

排他ロックをかけることで、対象となるファイルに対する他のプロセスのロック をブロックすることができます。ただし、ファイルに対する読み書き自体を禁止 するわけではありませんので、排他ロックを有効にするためには、読み込みモード でファイルを開くときに、次のように共有ロックをかける必要があります。

fo = open("data.txt", "r")
fo.flock(File::LOCK_SH)    #ファイルに共有ロックをかける。
...
ファイルからの読み込みなどの処理
...
fo.flock(File::LOCK_UN)    #ロックを解除する。
fo.close

共有ロックをかけておかないと、たとえファイルに排他ロックがかかっていたとしても、 ロックを無視して読み込んでしまいます。また、読み込みの最中に他のプロセスによって 書き込まれるのを防ぐためにも、共有ロックをかけておく必要があります。

課題1-2

ファイルのロックを行わないとどのようなことが起こるか、次の課題で確かめてみましょう。

課題1-2-1

次のようなアクセスカウンターのCGIプログラムを、sleep_counter.cgiというファイル名で作成して、ブラウザから実行できるようにしなさい。
実行できるようになったら、ブラウザのウィンドウをもう一つ開いて同じページを表示し、一方のウィンドウでは書き込む前に10秒間停止し、その間にもう一方のウィンドウで停止せずに書き込むとどうなるか確かめて報告しなさい。
また、なぜそのような結果になるか、考えて説明しなさい。

#!/usr/bin/ruby
require "cgi"
cgi = CGI.new
n = cgi["sleep"].to_i

if(n != 10)
  n = 0
end

#アクセス数を取得
fo=open("sleep_counter.txt", "r")
count = fo.gets.to_i
fo.close

count = count + 1

#アクセス数を保存
fo=open("sleep_counter.txt", "w")
sleep(n)        #ここでn秒停止。
fo.print count
fo.close

print <<EOS
Content-type: text/html; charset=euc-jp

<html>
<head>
<META http-equiv="Content-Type" content="text/html; charset=EUC-JP">
<title>アクセスカウンターのCGIプログラム(#{n}秒間停止)</title>
</head>
<body>
現在のアクセス数は <font size="7">#{count}</font> です。<br>
<br>
<br>
<a href="sleep_counter.cgi?sleep=10">10秒停止してから書き込む</a><br>
<br>
<a href="sleep_counter.cgi?sleep=0">今すぐ書き込む</a><br>
</body>
</html>
EOS

課題1-2-2

上のプログラムを書き換えて、ファイルのロックを行うようにしなさい。 書き換えたら、先ほどと同じように2つのウィンドウで開いて、ファイルのロックを正しく行えていることを確認しなさい。

課題1-2-3

上の課題で作成したCGIプログラムが、ロックを行ったことによってどのように動作するようになったか報告しなさい。 また、なぜそのような結果になるか、考えて説明しなさい。

オブジェクトをファイルに保存する

これまではプログラムで扱うデータを半角スペースで区切ってテキストファイルに 保存したり、カンマで区切るCSV形式に変換して保存してきました。 今回は、プログラムの中で扱うオブジェクトをそのままファイルに書き出したり、 書き出したオブジェクトをファイルから読み戻して使う方法を学ぶことにします。 このような方法をシリアライゼーション、もしくは マーシャリングと呼びます。

Rubyにはオブジェクトを簡単にシリアライズするための道具が用意されています。 課題を解きながら使い方から学んでいきましょう。

課題1-3

シリアライズの仕方を学ぶために、ハッシュ(Hashクラスのオブジェクト)を ファイルに保存し、読み戻して再び使用できるか確かめてみることにします。

課題1-3-1

次のプログラムを実行して、ハッシュオブジェクトをファイルにシリアライズしなさい。 実際にオブジェクトをファイルに保存することができているかどうか、プログラムを実行して 確かめること。
ファイルには、オブジェクトがバイナリーデータとして書き込まれます。作成されたファイルを 開いて中を確かめてみましょう。

menu = {"ショートケーキ" => "550円", "チョコレートケーキ" => "500円", "チーズケーキ" => "450円"}

fo = open("menu.dat", "w")
Marshal.dump(menu, fo)
fo.close

課題1-3-2

今度は保存したオブジェクトを読み戻して使用してみましょう。
次のプログラムを実行して、ハッシュをファイルから読み込み、実際に読み込んだ オブジェクトを使用できているかどうか確かめなさい。

$KCODE = "e"

fo = open("menu.dat", "r")
menu = Marshal.load(fo)
fo.close

p menu
p menu["ショートケーキ"]
p menu["チョコレートケーキ"]
p menu["チーズケーキ"]

課題1-3-3

シリアライズが可能なのは、もちろんHashオブジェクトだけではありません。 ほとんどのオブジェクトは問題なくシリアライズすることができます。 今度は、様々なオブジェクトを配列に入れてシリアライズし、読み戻して使用できるか 確かめてみることにします。
次のプログラムの配列aryを、シリアライズしてファイルに保存しなさい。 また、ファイルから読み込んで復元し、配列からオブジェクトを取り出してpを使って表示しなさい。

#配列に様々なオブジェクトを入れ、ファイルにシリアライズするプログラム

ary = []
ary << Time.now                        #Timeオブジェクト
ary << {1 => "a", 2 => "b", 3 => "c"}  #Hashオブジェクト
ary << "あいうえお"                    #Stringオブジェクト
ary << 1000                            #Fixnumオブジェクト

#この部分を作成する。
#ファイルから配列とその要素のオブジェクトを復元し、表示するプログラム

$KCODE = "e"

#この部分を作成する。

for obj in ary
  p obj
end

課題1-3-4

シリアライズしてファイルに保存できるのは、Rubyに元々用意されているクラスのオブジェクトだけではありません。 自分で定義したクラスのオブジェクトも、同じようにシリアライズすることができます。

これまでに自分で定義したクラス(どれでもよいです)のオブジェクトを、シリアライズしてファイルに保存するプログラムを作成しなさい。
例えば、クラスを用いて実現した「アドレス帳ファイル」のプログラムにおいて、ユーザーアカウントなどのデータベースのオブジェクトを生成してシリアライズする、 ということを試してみてください。

課題1-3-5

課題1-3-4でシリアライズしたデータベースのオブジェクトを、ファイルから読み込んで復元するプログラムを作成しなさい。

ただし、シリアライズしたオブジェクトを復元するときには、そのオブジェクトのクラスが定義されている必要があります。

require "復元するオブジェクトのクラス"    #もしくはクラスの定義が書いてあるファイル

[CGIプログラムでブラウザから受け取った値をシリアライズするときの注意点]

フォームやパラメータから受け取った文字列をシリアライズして復元するときに、次のようなエラーが発生することがあります。

./kadai1-3.rb:15:in `load': undefined class/module CGI:: (ArgumentError)
        from ./kadai1-3.rb:15:in `load'

これは、「シリアライズされたオブジェクトのクラスが定義されていない」ことを示すエラーメッセージです。 この場合、CGIクラスが未定義のため、上のようなエラーメッセージが表示されたことになります。 このようなときは、requireを使ってCGIクラスをロードし、利用できるようにしておきます。

require "cgi"

たとえCGIクラスを使用する必要がなくても、CGIクラスを用いて取得した値を保存した場合は、復元時にCGIクラスをロードしておかなければ、オブジェクトを復元することができないのです。

[シリアライズを行うときのその他の注意点]

シリアライズはオブジェクトをそのままファイルに記録できますし、復元した後もすぐに元通りのオブジェクトとして扱うことができますので、これまでの方法と比べてとても便利です。

ただし、ファイルにはオブジェクトがバイナリーデータとして書き込まれますので、ファイルを開いてデータの内容を確認したり、エディターを使ってデータを直接編集することはできません。 そのようなことを行いたいときは、これまでのようにテキストファイルに保存するか、CSV形式で保存する方法を選択するとよいでしょう。

また、自分で定義したクラスのオブジェクトをシリアライズする場合には、復元する際にクラスの定義が必要になります。 クラスの定義のプログラムが失われたり、大きく改変されてしまうと、バイナリーデータから情報を復元することができなくなってしまいます。 後々困ったことにならないように、シリアライズされたデータだけでなく、クラスの定義ファイルもしっかり管理しておく必要があります。

戻る

;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;