このページは福井県立大学の田中求之が2006年1月まで運用していた Mac のサーバ運用に関する会議室 「Web Scripter's Meeting」の記録です。情報が古くなっている可能性がありますのでご注意ください。

CGI の CONDITIONAL_GET 対応

発言者:田中求之
( Date Monday, August 25, 1997 16:58:27 )


●CONDTIONAL_GET とは

ブラウザは、過去にアクセスしたことがあって手持ちのキャッシュが有効な
ページの場合、サーバーに対して「データが更新されていたならば新しいデー
タを送ってくれ」というリクエストを発行するようになっています。

もしページが、ブラウザの言ってきた日付以降に更新されていたならば、通常
のリクエスト同様に、ページのデータを送り返します。

一方、データの更新が行われていなかった場合には、サーバーはブラウザに対
して「データは変更されていないよ」という情報だけを返します。これを受け
てブラウザは手持ちのキャッシュを使ってページを表示するわけです。この場
合、「更新されていない」という情報がやり取りされるだけで、ページのデー
タの転送処理は行われませんので、ページの表示が早くなるわけです。

これが CONDITONAL_GET といわれるものです。つまり、ブラウザが持っている
キャッシュをうまく利用することで、少しでもトラフィックを減らし、反応速
度を上げようとするための工夫です。

CGI においては、一般的には、アクセスの度にデータが変わりますので(アク
セスの度に動的にページの変更が行えるのが CGI のメリットなのですから)、
この CONDITIONAL_GET のことは考慮する必要がないのですが、たとえば会議室
システムのように、場合によっては過去と同じデータを何度も送りだすことに
なる(書き込みなどが行われなかった場合) CGI の場合には、
CONDITIONAL_GET に対応することでアクセスの反応速度を改善することが可能
になります。

そこで、ここでは、CGI を CONDITIONAL_GET に対応させる方法について説明し
ます。

なお、CONDITIONAL_GET に対応させるためには、ブラウザからサーバーに送ら
れてくるリクエストの HTTP ヘッダーのすべてを CGI が受け取れなくてはなり
ませんので、CGI に対して full_request をパラメーターで渡すようになって
いる web サーバーを使用する必要があります(WebSTAR, Quid Pro Quo な
ど)。

田中求之 さんからのコメント
( Monday, August 25, 1997 16:59:02 )

●CONDITONAL_GET の実際

CONDITONAL_GET に対応させるポイントは、ブラウザからのリクエストのヘッ
ダー、および、CGI からブラウザへ返すヘッダーを、適切に処理することにあ
ります。

まず最初に具体的にどのようなやりとりがおこなわれるかを示します。これを
理解すれば、あとは CGI にヘッダーの処理を追加すればよいことになります。

CONDITIONAL_GET の場合には、以下のようなリクエストがサーバーに対して送
られてきます。

GET /test.html HTTP/1.0
If-Modified-Since: Monday, 25-Aug-97 06:46:19 GMT; length=267
Connection: Keep-Alive
User-Agent: Mozilla/2.02 (Macintosh; I; PPC)
Pragma: no-cache
Host: 157.6.48.3
Accept: image/gif, image/x-xbitmap, image/jpeg, image/pjpeg, */*
Accept-Language: en, ja


1行目は通常の GET (ページのリクエスト)と全く同じなのですが、2行目の 
If-Modified-Since: というヘッダーによって、ブラウザが持っているキャッ
シュのデータの更新日を告げており、これよりもページのデータが新しければ
データを送って欲しいということを意味しています。GMT で示されたページの
データの最終更新日と、データの長さが示されています。

これを受け取ったサーバーの側では、もしブラウザの言ってきた日付以降に
データが更新されていたならば、通常の GET と同様にページのデータを送り返
します。

一方、ブラウザの言ってきたデータの更新日が、実際のページのデータの更新
日と同じ、つまり前回アクセスした時点からデータの更新がなかった場合に
は、

HTTP/1.0 304 Not Modified
Date: Mon, 25 Aug 1997 06:59:31 GMT

というヘッダーのみを送り返します。これをうけとったブラウザは、手持ちの
キャッシュを表示することになります。

田中求之 さんからのコメント
( Monday, August 25, 1997 16:59:44 )

ここで気をつけないといけないのは、ブラウザが知らせてくるのは、データの
最終更新日であって、キャッシュの最終更新日ではないということです。つま
り、ブラウザが CONDITONAL_GET を発行するためには、ブラウザがデータの最
終更新日を知っていなければならないということになります。データの最終更
新日が分かっている場合のみ、ブラウザは CONDITONAL_GET のリクエストを発
行します。

ブラウザはデータの最終更新日をどのようにして知るか? 当然のことです
が、サーバーから教えてもらわなければ知りようがありません。つまり、ブラ
ウザが CONDITIONAL_GET を利用できるように、サーバーからブラウザに対し
て、データの最終更新日も知らせておく必要があります。もちろん、この情報
はサーバーからブラウザへデータを送る際の HTTP ヘッダーによって伝えられ
ます。

実際、HTML で書かれたページや画像を送りだす際には、以下のように、サー
バーは HTTP ヘッダーの中でデータの最終更新日をブラウザに対して知らせて
います。

HTTP/1.0 200 OK
MIME-Version: 1.0
Server: WebSTAR/2.1 ID/33602
Message-ID: <b0223c30.13623@mtlab.ecn.fpu.ac.jp>
Date: Mon, 25 Aug 1997 07:06:16 GMT
Last-Modified: Mon, 14 Apr 1997 06:57:49 GMT
Content-type: text/html
Content-length: 1833


Last-Modified ヘッダーの部分がデータの最終更新日です。上の場合ですと、
97年4月14日(ただし GMT)ということです。この情報をもとに、ブラウ
ザは CONDITIONAL_GET のリクエストを行うわけです。このように、
CONDITIONAL_GET に対応する場合には、通常のアクセスにおいて、サーバーが
データの最終更新日を知らせるようになっている必要があるわけです。


以上が、実際のアクセスにおいてどのように CONDITIONAL_GET が行われている
かという具体例です。ポイントをまとめておくと、以下のような流れになりま
す。

1:サーバーは、データを送りだす際に、データの最終更新日を Last-
Modified: ヘッダーでブラウザに伝える

2:ブラウザは、2度目以降にそのページにアクセスする際には、If-
Modified-Since: ヘッダーによって手持ちのキャッシュに記録されているデー
タの最終更新日をサーバーに対して伝える

3:サーバーは、ブラウザの知らせてきた更新日と、実際のデータの更新日を
比較して、もし同じであれば「変更なし (Not Modified) 」というヘッダーを
返す。もしデータが更新されていた場合には、新しいデータを送り返す

田中求之 さんからのコメント
( Monday, August 25, 1997 17:11:31 )

●CONDITONAL_GET 対応のスクリプト

では、実際に CGI の側で CONDITIONAL_GET (以下 C_GET と略記)に対応させ
る方法です。具体的な例を示しながら説明したほうが分かりやすいと思います
ので、ここでは search_arg で指定したファイルを読み込んで、そのデータを
送り返すという、簡単な CGI をもとにして、それを C_GET に対応させていく
手順を示します。説明は AppleScript で行います。また、この CGI を実際に
動かすには Tanaka's osax 1.1 以降が必要です。

まず C_GET に対応させていないスクリプトです。

property crlf : (ASCII character 13) & (ASCII character 10)

property http_10_header : "HTTP/1.0 200 OK" & crlf ツ
  & "MIME-Version: 1.0" & crlf ツ
  & "Content-type: text/html; charset=Shift_JIS" & crlf ツ
  & crlf

property thisFldr : ""

on run
  set oldDel to AppleScript's text item delimiters
  set AppleScript's text item delimiters to {""}
  set myPath to path to me
  tell application "Finder"
    set thisFldr to (folder of myPath) as string
  end tell
  set AppleScript's text item delimiters to oldDel
end run


on ヌevent WWWスsdocネ path_args ツ
  given ヌclass kforネ:http_search_args, ヌclass Kfrqネ:full_request
  
  try
    
    set myPage to item 1 of (DecodeJArgs http_search_args)
    
    set myFile to thisFldr & myPage
    set myData to readFromFile file myFile
    
    
    return http_10_header & "<TITLE>" & myPage & "</TITLE>" & crlf ツ
      & "<PRE>" & myData & "</PRE><HR><P>" & crlf ツ
      & "<H4>Full_request</H4><PRE>" & full_request & "</PRE>"
    
  on error errMsg number errNb
    
    return http_10_header & crlf ツ
      & "<TITLE>Error</TITLE>" & crlf ツ
      & "<BODY BGCOLOR=\"#FFFFFF\" text=\"firebrick\">" & crlf ツ
      & "<h3>実行中に以下のエラーが生じました</h3>" & crlf ツ
      & "<HR><P><TT>" & errMsg & "</TT>"
  end try
  
end ヌevent WWWスsdocネ


田中求之 さんからのコメント
( Monday, August 25, 1997 17:55:42 )

この CGI を C_GET.acgi といった名前でアプリケーションにしておき、サー
バーと同じフォルダーに入れておいて、以下のような URL でアクセスすると、
test という名前のファイルの内容と、ブラウザがサーバーに対して送ったリク
エストが表示されるようになっています。

  http://your.host/C_GET.acgi?file=test


→  C_GET.acgi.txt.hqx

田中求之 さんからのコメント
( Monday, August 25, 1997 18:24:10 )

● Last-Modified ヘッダーの追加

C_GET に対応させる最初のステップとして、データを送り返す際に、データの
最終更新日をブラウザに知らせるようにします。このためには、ページのファ
イルの最終更新日の情報を得たうえで、HTTP ヘッダーにLast-modified ヘッ
ダーを追加することになります。この際、日付データを GMT(グリニッジ標準
時間)に変換したうえで、ヘッダー用の書式に変換する必要があります。ヘッ
ダーでは、

  Mon, 25 Aug 1997 06:49:53 GMT

という書式にしなければなりません。

具体的には、以下のような処理を追加することになります。

まずページのファイルの最終更新日の情報を得て、それを GMT に変換します。

set xDate to getFileModDate file myFile
set xDate to xDate - (time to GMT)

つぎにその日付をヘッダー用の書式に変換します。

set dateStr to (DateToHeaderStr xDate with as1123) & " GMT"

なお、現在の日付も Date ヘッダーとして追加しますので、

set yDate to (current date) - (time to GMT)
set nowDateStr to (DateToHeaderStr yDate with as1123) & " GMT"

というスクリプトで現在時間のヘッダー用データも作っておきます。

そして、これらを使って CGI が返す HTTP ヘッダーの中に必要なヘッダーの追
加を行います。

  return "HTTP/1.0 200 OK" & crlf ツ
    & "Date: " & nowDateStr & crlf ツ
    & "MIME-Version: 1.0" & crlf ツ
    & "Content-type: text/html; charset=Shift_JIS" & crlf ツ
    & "Last-modified: " & dateStr & crlf ツ
    & crlf ツ
    & "<TITLE>" & myPage & "</TITLE>" & crlf ツ
    & "<PRE>" & myData & "</PRE><HR><P>" & crlf ツ
    & "<H4>Full_request</H4><PRE>" & full_request & "</PRE>"

( property の http_10_header を使わずに、直接に HTTP ヘッダーを組み立てています)

これによって、ブラウザに対してデータの最終更新日が知らされるようになり
ます。ですから、次のアクセスからは、ブラウザは C_GET のリクエストを発行
するようになります。実際にスクリプトを動かして、reload したときの 
full_request の内容の変化に注意してください。2度目からは If-Modified-
Since ヘッダーが現れるはずです。


→  C_GET.acgi.txt2.hqx

田中求之 さんからのコメント
( Monday, August 25, 1997 19:02:18 )

●If-Modified-Since への対応

続いて、ブラウザからの If-Modified-Since ヘッダーの情報をもとに、データ
の更新日をチェックする処理を組み込みます。

手順としては、full_request から  If-Modified-Since ヘッダー部分を抜き出
し、ここに記されている更新日の情報を AppleScript の Date オブジェクト
(日付型データ)に変換する処理を行います。そして、それを実際のページの
更新日(スクリプトでは xDate という変数に収まっています)と比較を行い、
もし更新されていなければ Not Modified というステータスを返すようにしま
す。

まず full_request から更新日の情報を得るという処理ですが、これは少し面
倒なことになります。というのも、ブラウザが送ってくる日付のデータは

  Monday, 25-Aug-97 06:46:19 GMT

という書式になっているので、これを処理して日付データにするという文字列
の処理を行わなければならないからです(念のため Mon, 25 Aug 1997 
06:49:53 GMT という書式の日付にも対応させておくことにします)。

この部分は、以下のようなハンドラーとして独立させておきましょう。

on ifModCheck(full_req)
  
  set Tm to item 1 of (pickUpFromData full_req startOf "If-Modified-Since: " endOf return with Trim)
  set tgList to itemsToList Tm itemDelimiter " "
  
  if "-" is in Tm then
    
    --- Date Style = "Monday, 04-Aug-97 12:19:59 GMT"
    
    set dList to itemsToList (item 2 of tgList) itemDelimiter "-"
    set tList to itemsToList (item 3 of tgList) itemDelimiter ":"
    
    set xYear to ((item 3 of dList) as integer) + 1900
    set xmonth to mNameTomNum(item 2 of dList)
    set xDay to (item 1 of dList) as integer
    
  else
    --- Date Style = "Tue, 05 Aug 1997 01:09:40 GMT"
    
    set tList to itemsToList (item 5 of tgList) itemDelimiter ":"
    set xmonth to mNameTomNum(item 3 of tgList)
    set xYear to (item 4 of tgList) as integer
    set xDay to (item 2 of tgList) as integer
    
  end if
  
  set xdList to {xYear, xmonth, xDay, (item 1 of tList) as integer, (item 2 of tList) as integer, (item 3 of tList) as integer}
  
  return (DateListToDate xdList)
  
end ifModCheck


on mNameTomNum(xmonth)
  if xmonth = "Jan" then
    return 1
  else if xmonth = "Feb" then
    return 2
  else if xmonth = "Mar" then
    return 3
  else if xmonth = "Apr" then
    return 4
  else if xmonth = "May" then
    return 5
  else if xmonth = "Jun" then
    return 6
  else if xmonth = "Jul" then
    return 7
  else if xmonth = "Aug" then
    return 8
  else if xmonth = "Sep" then
    return 9
  else if xmonth = "Oct" then
    return 10
  else if xmonth = "Nov" then
    return 11
  else if xmonth = "Dec" then
    return 12
  end if
end mNameTomNum

田中求之 さんからのコメント
( Monday, August 25, 1997 19:19:11 )

ifModCheck() ハンドラーは、full_request をパラメーターとして受け取る
と、その中の If-Modified-Since ヘッダーの部分にかかれている日付を抜き出
して、それを AppleScript の Date オブジェクトに変換して返すようになって
います(時間は GMT のまま)。

If-Modified-Since ヘッダーが見つからなかった(通常の GET によるアクセス
の場合など)にはエラーを返します(エラーを起こします)。

CGI の処理としては、この ifModCheck() ハンドラーを try のブロックの中で
呼び出して、そこで得られた日付データをもとに判断を行うことになりますの
で、以下のような処理になります。

try
  set zDate to ifModCheck(full_request)
on error
  set zDate to xDate - 100
end try

if zDate = xDate then
  return "HTTP/1.0 304 Not Modified" & crlf ツ
    & "Date: " & nowDateStr & crlf ツ
    & crlf
end if

もし C_GET によるアクセスが行われていた場合には、変数 zDate にブラウザ
が知らせてきた最終更新日が収まります。一方、普通の GET の場合には、
ifModCheck() はエラーを起こしますので、on error に処理が飛んで、zDate 
には zDate より 100 秒だけ古い日付データがおさまります。

そして、xDate と zDate の比較を行い、両者が等しければ、データの更新は行
われていないわけですから、Not Modified のステータスを返します。両者が食
い違う場合には、通常のファイル読み込み&データ転送を行います。

以上の処理によって、CGI が C_GET に対応した動作を行うことが可能になります。

C_GET 対応の処理をすべて組み込んだスクリプトは↓です。

以上、簡単ですが CGI で CONDITIONAL_GET に対応させる方法の説明でした。

→  C_GET.acgi.txt3.hqx

田中求之 さんからのコメント
( Monday, August 25, 1997 19:24:35 )

CGI に何度かアクセスをして、サーバーのログの送りだしデータ量の変化を見てください。
2度目からは、ヘッダーのみ(66 バイト)を送りだしているはずです。

ファイルに変更を加えたり、スーパー Reload (Option を押しながら Reload)
を行ってみて、動作を確かめてもらうとよいと思います。


田中求之 さんからのコメント
( Wednesday, September 17, 1997 01:08:57 )

この発言に書いたことを PDF ファイルにして登録しておきました。
(Acrobat Reader 3.0J が必要です)


→  C_GET_pdf.hqx

田中求之 さんからのコメント
( Wednesday, September 17, 1997 22:47:30 )

ついでに、PDF をオンラインで見られるようにもしておきました。全11ページですが
25K とサイズが小さいので、Byte Serving する間もなくダウンロードが終わってし
まうと思いますが (^_^;;


→  C_GET PDF