304 Not Modifiedをhttpレスポンスヘッダで制御する方法(If-Modified-Since偏)

· application, ネットワーク, 携帯

携帯3キャリアのキャッシュ制御をしようとしてたら、304で返すのがいいっぽいということで、まずPC(firefox)で304 Not Modifiedを制御する方法を実装してみました。

前提条件ですが、

  • 制御対象が画像なので、metaタグによる制御は使えない

  • 画像のurlは /img/1 といった形になり、実ファイルではなく、phpによる処理が入り、かつ、urlが変わらず返す画像が変わる場合がある(後述)

  • 基本的にはキャッシュさせないが、特定の条件(後述)時にのみキャッシュさせる

  • キャッシュ期間を設定したい

■ゴール

最終的には携帯に対して制御したいので、ローカルor途中のゲートウェイ等でキャッシュを利用されたくない。つまり、状況に応じて期間付きの304を返すレスポンスヘッダの制御をする。(参考:携帯にキャッシュさせる方法(キャッシュコントロールについて)

以上を踏まえての実施結果になります。

処理概要

下記のようなhtmlソースを想定

<img src="/img/1" />

直接画像ファイルを指定するのではなく、URL変換(mod_rewrite)によって、画像ファイルを指定します。さらに間にphpも動きます。phpが行う処理は大きく3つで、

  • リクエストしてきたユーザのセッションからログイン状態を見る

  • ログインしていなければ、サンプル画像を、していれば通常画像を選ぶ

  • 画像に応じたレスポンスヘッダを設定し、画像を返す

ログイン前は、キャッシュさせずに毎回サンプル画像を返し、ログインしたら、一度通常画像をキャッシュしたらあとは304を返すのみ、という状態にします。

レスポンスヘッダの設定

まず、サンプル画像に関してはキャッシュさせず、通常画像をキャッシュさせます。(*本来両方キャッシュさせたいのですが、今回の方法では難しいと判断して通常画像のみにしてます)

キャッシュさせない場合のレスポンスヘッダのphp

$defalut_headers["Expires"] = "Thu, 01 Jan 2000 00:00:00 GMT"; // 有効期限を過去日付に設定
$defalut_headers["Cache-Control"] = "no-cache, no-store, must-revalidate";
$defalut_headers["Pragma"] = "no-cache";
$defalut_headers["Last-Modified"] = gmdate("D, d M Y H:i:s",getlastmod()) ." GMT";  // 最終更新日を設定

ログイン後のキャッシュさせるレスポンスヘッダのphp

$option_headers["Last-Modified"] = gmdate("D, d M Y H:i:s",time()) ." GMT";  // キャッシュ期間用に今の時間に偽装
$option_headers["Cache-Control"]= "max-age=0"; // ローカルキャッシュを使わせないための設定
$option_headers["Pragma"]= ""; // キャッシュさせるために明示的に空にする

ログイン後のヘッダは、ログイン前のヘッダに上書きする形になります。

こんな感じ

foreach($defalut_headers as $key => $v){
	$headers[$key] = isset($option_headers[$key]) ? $option_headers[$key] : $v;
}
// ヘッダー出力
foreach($headers as $key => $v){
	header($key.":" . $v);
}

レスポンスヘッダの中身

実際に出力されるヘッダ(関係なさげなヘッダは除いてます)。ログイン直後を想定していますので、今までキャッシュされず、今回からキャッシュさせる動きになります。

リクエスト1回目

GET /img/1 HTTP/1.1

Host: domain

(中略)

Pragma: no-cache

Cache-Control: no-cache

最初のリクエストは、画像を直接Ctrl+F5の強制リロードでリクエストしてます。

レスポンス1回目

で、そのレスポンス1回目。

HTTP/1.1 200 OK

Date: Wed, 17 Feb 2010 13:44:09 GMT

Server: Apache/1.3.41 (Unix) mod_ssl/2.8.31 OpenSSL/0.9.8e

Cache-Control: max-age=0

Expires: Thu, 01 Jan 2000 00:00:00 GMT

Last-Modified: Wed, 17 Feb 2010 13:44:09 GMT

Content-Type: image/gif

ブラウザにキャッシュさせるよう設定したExpires,Last-Modified,Cache-Controlが反映されています。

で、これで一旦これらのファイル情報がブラウザに保存されます。

リクエスト2回目

そこでリクエスト2回目を投げます。

GET /img/1 HTTP/1.1

Host: domain

(中略)

If-Modified-Since: Wed, 17 Feb 2010 13:44:09 GMT

Cache-Control: max-age=0

If-Modified-Sinceというヘッダが付加されていますが、一回目のリクエストでLast-Modifiedがあったため、その値をブラウザが投げています。

これは、キャッシュしているファイルの最終更新日をサーバに投げて、サーバ側でそのファイルの最終更新日を比較させるためです。今回はそれを利用します。そのコードがこちら

if((time() - strtotime($_SERVER['HTTP_IF_MODIFIED_SINCE'])) < $default_term){
	header( 'HTTP/1.1 304 Not Modified' );
	header( 'Content-Type:' . $option_headers["Content-Type"] );
	exit();
}

$_SERVER[‘HTTP_IF_MODIFIED_SINCE’]にはIf-Modified-Sinceの値が入っています。つまり

(time() - strtotime($_SERVER[‘HTTP_IF_MODIFIED_SINCE’]) =

キャッシュさせようとした最初のリクエストから今のリクエストまでの時間差

ということになります。これを、$default_termと比較しています。$default_termが304を返す期間(秒)になりますので、たとえば60と入れておくと1分間304を返すような設定になります。

また、1回目のレスポンスでCache-Controlをたとえばmax-age=100としてると、100秒間この画像へのリクエストは行われず、ブラウザは保存したキャッシュを表示するのみとなります。そのためmax-age=0とすることで確実にリクエストを投げてくれます。

レスポンス2回目

次に、それを受けたレスポンス2回目です。

HTTP/1.1 304 Not Modified

Date: Wed, 17 Feb 2010 13:44:15 GMT

Content-Type: image/gif

無事、304が帰ってきました。また、$default_termを過ぎて再度リクエストを投げると、304を返す条件を満たさないため、1回目のレスポンスとほぼ同じ結果が返ってきました。

まとめ

まずやりたかったのは、状況に応じて期間付きの304を返すレスポンスヘッダの制御をする、でした。そこで肝となったのは、リクエストヘッダであるIf-Modified-Sinceでした。こいつがブラウザが送ってくる時間的な情報だったので、

  • ログイン後、ヘッダを変更!

  • Last-Modifiedを今の時間に偽装!

  • If-Modified-Sinceを受け取って今の時間と比較!

  • 期間内なら304!

という形で実装できました。

次は、これらが携帯の各キャリアで通じるかどうかですねw

それとauでは利用できないようですが、Etagも並行して実装したほうが確実かな?とおもうので、またそのあたりできたらエントリーしてみたいと思います。(エントリーしました。

このあたりの知識はまだまだ、甘い点がありますので、何か間違いあればご指摘いただければと思います。

画像未復旧: hatena.gif

関連があるかもしれないエントリー