一般的な更新チェッカ —— 良きWWWDであれ、WWWCであれ、ウェブ上のアンテナであれ —— に捕捉されないサイレント更新は比較的容易だ。しかし、ブラウザからの If-Modified-Since リクエストを自力で適切に扱うことは比較的めんどうくさい。このメモでは、両方について説明する。
クライアント側からみると更新チェッカは更新されているサイトを効率良くまわるためのものだが、サーバ側には別の意味がある。前にも書いたが、「更新されてないのに(つまり、あなたのローカルにすでに同じキャッシュがあるのに)再度 GET されるのは、あまりありがたくない」。いちいち数十ないし数百キロバイトのHTMLファイル全文をブラウザで取得せずとも、HTTPレスポンスヘッダだけで —— すなわち、HEAD リクエストを送ってもらえれば —— 実体を1バイトも送らずに、更新されたかどうかお知らせできるからだ。
このことは、大手プロバイダにホストされているこじんまりとしたサイトなどでは、事実上、問題にならないだろう(むしろ何度もリロードされるとカウンターの数値が増えてうれしい、という考えもあるかもしれない)。けれど、いまどきのページは、動的に生成されることが多い。特にファイルが千、二千、あるいは万単位であるようなサイトでは、完全にスタティックに管理するのは、まず無理だろう。概念的にいうと、ブラウザからは小さなHTMLファイルにしか見えなくても、裏方では、アーカイブ(データベース)から最近の記事だけを読み出して、相対リンクを適切に書き換えたうえで、トップページを構築する、といった、けっこうややこしい処理が行われていることが多い(特にフローティングスレッドな掲示板とか)。
このように動的に作られているページは、つねにリクエストされた瞬間に新たに作られるのだから、見るたびに異なる可能性があって、素朴にいえば「最終更新時刻」という概念がない。実際、デフォルトではアパッチは Last-Modified ヘッダを返さない(ので、つねに全文GETされる結果になる)。しかし、アパッチには XBitHack というハックがあるので、SSIなページに Last-Modified をつけることは簡単だ。CGIな掲示板でも、新規書き込みがないかぎり、自分で更新されてないと言って良い。PHP でも同様だ。
表紙を index.cgi にしているかたが「5分おきにWWWCをかけるのは、やめてください」と書いているのをみたことがあるが、これなども、Last-Modified をつけないのでいちいちぜんぶの読み込みファイルがオープンされて同じ料理を何度も作らされるハメになるのだろう。NPHスクリプトを使わなくても、Content-Type: Text/html のあたりに Last-Modified: Fri, 07 Sep 2001 04:07:00 GMT のような、RFC1123 のスタンプを入れておけば、だいたいうまくいく。
ひるがえって、ページを生成するとなると、けっこう時間がかかる。複雑な処理が入ると、1ページの生成に0.1秒オーダのコストがかかることもある。サーバでは複数のプロセスを平行的に処理できるとは言うものの、典型的な環境では、ちいさなサーバでも、毎秒、数十から百単位のリクエストにこたえなければいけないのであって、0.1秒といえば、ばくだいなコストなのだ。転送量のほうは、大したことなくても、0.1秒も排他処理をしていると、アクセスがたてこんだときにどんどん待ち行列が伸びて、ロックエラーが起きたりするだろう。
このような場合、自分で自分をキャッシュする(静的なHTMLに書き出す)とか Last-Modified をつけるか、少なくとも、なんの手間もかからない XBitHack の使用は検討に値する。このへんの話はウェブ上を検索すればいくらでも情報が得られる。
それにしても、ちょっと誤字をなおすとかのつまらないことで、「更新」と認識されてしまう。この場合、更新チェッカ等の報告で「あ!更新された」とよろこんで来てくださる訪問者の期待を裏切ることになるばかりか、上記のように、サーバリソースの無駄遣いにもつながる。とは言うものの、ウェブは気づいたそばからどんどん書き足し書き換えるところ、つまり動的なところに大きな意味がある。現時点では、まだ過渡期で、サーバリソースも帯域も不足気味なので、こんなつまらないことで悩まざるを得ないのだろう。
ともあれ、HTTPレスポンスヘッダを自分で作ってしまえば、更新チェッカの一般的なスキャンに「更新」が映らないようにすることもできる。
まず、更新チェッカ側もラクをしたいので、HEAD リクエストで Last-Modified が返れば、それを採用するのがふつうだ。これを「ごまかす」のは容易だ。
少し難しいのは、GET リクエストに if-modified-since がくっついてきた場合で、304 Not Modified とこたえてヘッダだけでうち切ってしまえばステレス自体は簡単だが、そうやってサーバからコントロールをうばったからには、自分で後始末をしないといけない。つまり、更新されたと認識されてもいい更新のときは、200 OK でファイルを転送すべきなのだが、その場合、いつも 200 OK と本文を送り返しては本末転倒だろう —— if-modified-since (わたしは、これこれの時点のキャッシュを持っています。それ以降に更新されていたら送ってください)と向こうからクールなリクエスト(お互いラクしましょう)をしてるのに、つねに全文転送するのは、いかにもよろしくないし、本来の趣旨にも反している。
通常、サーバが if-modified-since を解釈してうまく制御してくれるわけだが、その制御を乗っ取る以上(NPH にしてヘッダをこちらでコントロール)、なるべくサーバがやるのと同じに見えるように、スクリプト側でエミュレートしなければいけない。
この場合、ブラウザは(技術的には少しややこしいが実際のところ)通常は rfc1123 形式のスタンプを送ってくる。こちらも、Last-Modified ヘッダ用の同じ形式のスタンプが手元にある。問題は、
Fri, 07 Sep 2001 04:07:00 GMT
Sun, 26 Aug 2001 09:55:59 GMT
のような2つのヘッダを比較して、未来になっているかどうかの判定だ。
がむしゃらにやれば判定できないことは、ないが、とりあえず次のように考えるのが賢明だろう。すなわち、ブラウザが持っているキャッシュと手元のタイムスタンプが一致するなら、サーバとクライアントのコンテンツは同期しているのだから、更新されてないと判定して良いだろう。次に、両者が一致しない場合は、いちおう更新されていると判断してかまわない。サーバ側より未来のキャッシュがクライアント側にあるわけないので、時間が一致しないとしたら、サーバ側のほうが未来、と考えるわけだ。
天文学というか暦学の「ユリウス日」を使うと、もっとちゃんと判定することもできる。このような判定が必要なのは、if-modified-since の基準時点として、キャッシュのスタンプの代わりに「前回おれさまがそこを訪問したのは、いついつだが、あれから更新されとるかい」という感じで時刻を送られた場合で、この場合、ずっと更新していなければ、相手の時刻のほうが最終更新より未来という形で不一致になるので、それを認識できなければならない。
sub parse_rfc1123() { local($_) = $_[0]; my %MONTH = ( 'Jan'=>1, 'Feb'=>2, 'Mar'=>3, 'Apr'=>4, 'May'=>5, 'Jun'=>6, 'Jul'=>7, 'Aug'=>8, 'Sep'=>9, 'Oct'=>10, 'Nov'=>11, 'Dec'=>12 ); my($D, $M, $Y, $h, $m, $s) = (split(/[ |:]/))[1..6]; $M=$MONTH{$M}; return (0) unless $M; my($_Y, $_M, $_D); if( $M==1 || $M==2 ) { $_Y = $Y-1; $_M = $M + 12; } else { $_Y = $Y, $_M = $M; } $_D = $D + $h/24 + $m/1440 + $s/86400; my $A = int($Y/100); my $B = 2 - $A + int($A/4); return ( int(365.25*($_Y+4716)) + int(30.6001*($_M+1)) + $_D + $B - 1524.5, $Y, $M, $D, $h, $m, $s ); }
このコードは位置天文学の基本中の基本とも言える「JD公式」を使ってRFC1123形式のタイムスタンプを通算の数値に変換し、長さ7の配列を返す(これはスケルトン。実装ではパラメータの正当性をチェックしたほうが良い)。どこまでもウェブに迷惑をおよぼすネスケ4の
Fri, 07 Sep 2001 04:07:00 GMT; length=28527
といった変なリクエストヘッダも扱えるロジックのハズだ。
返る配列の一番めは小数点以下を含むユリウス日で、UNIXエポックの限界(1970年)を超えて1582年までさかのぼれるので、工夫しだいでほかの用途にも使える。未定義値を渡されると0を返す。2番め以下は、年、月、日、時、分、秒で、ここでは使わないが、同じ手間なので返しておく。そんなわけで、
my ($your_jd) = &parse_rfc1123( $ENV{'HTTP_IF_MODIFIED_SINCE'} ); my $my_jd = 0; if($your_jd) { ($my_jd) = &parse_rfc1123( $last_modified ); } my $status = ($your_jd && $my_jd <= $your_jd )? "304 Not Modified" : "200 OK"; print "$proto $status\r\n"; print "Date: $date\r\n"; print "Server: $ENV{'SERVER_SOFTWARE'}\r\n"; print "Last-Modified: $last_modified\r\n"; # ほか必要なヘッダを入れる print "Connection: close\r\n"; print "Content-Type: text/html\r\n"; print "\r\n";
‥‥のようにすれば、いちおう「正しい」反応ができる。そのうえで、場合によっては更新されているけれど304を返して接続を切る、というのもお互いのためだろう。
帯域やリソースに比べてアクセスの多めなサイト(言い換えれば、アクセスに比べてリソースが不足気味なサイト)では、「更新」と認識されたとたん、多くのかたがいっせいに更新チェッカをクリックしてブラウザにロードすることによるトラフィックの山が無視できないと思われる。実際におもしろい記事をうちあげたのならさっそく読んでもらうのは良いことかもしれないが、ほんのちょっと引用符が抜けてたのを修正したとかで同じようなアクセス集中があると、サーバ側に負担になるのはもとより、せっかく見に来ても「あれれ、どこが更新されたんだ?」というつまらない思いをすることになってしまうであろう。
準備中のファイルを、アップロードしていてもパーミションを落として見えなくしておくことは広く行われている。同様に、更新とは言えないような更新は更新チェッカから見えなくするのは、あるいは許されるのでは、ないか。もちろん、その場合でも、前回の「更新と言えるような更新」より古いキャッシュを持っている人が if modified と言ってきたら「更新されてますよ~」と最新コンテンツを送り返すのは当然であるが、相手が最新版と実質的に変わらない直近のものをキャッシュしている場合には「変わってない」ことにするのも、ある意味、お互いのためでは、ないだろうか。とりわけ、毎日何度も定期チェックをかけてくる相手だと分かっていれば、次にちゃんとした更新があったときにまとめて新規取得してもらえばいい、という考えもありえよう。
なお、このメモの発想は、いつものように版権フリーですが、逆の意味で乱用したりするとあなたのサイトの訪問者やウェブが混乱する原因にもなりえるので、充分にご注意ください。ここに書いたようにしなければならない、するべきだ、という意味でなく、単にこういうこともやればできる、という話であって、予期可能な、あるいは予期せぬネガティブな副作用がありえることにご注意ください。