Qtブログ(日本語)

Qt での HTTP におけるキャッシュについて

作成者: 鈴木 佑|May 20, 2011 9:54:47 AM

この記事は Qt Blog の "HTTP caching with Qt” を翻訳したものです。
執筆: Peter Hartmann 2011年4月29日

この記事では HTTP のキャッシュ が一般的にどのように動作するのかと、Qt ではどうなっているのかを詳細に解説します。

HTTP のキャッシュとは?

ブラウザがウェブページを読み込む際には様々なリソース(HTML ページ、画像、CSS スクリプト等)がローカルに保存されます。これは同じリソースが再度読み込まれる際に、それをネットワークから再度取得する代わりにローカルに保存されたものを再利用できるかもしれないからです。これはにいくつかの利点があります。

  • 高速化: キャッシュからリソースを読み込むことでネットワークから取得するのに比べて大幅に時間を短縮できます。
  • オフラインでの読み込み: ネットワークに繋がっていなくてもページを表示できる可能性があります。
  • 読み込みを減らす: キャッシュやプロキシからリソースを読み込むことでサーバーへの負荷を減らすことができます。

この記事では、リソースがキャッシュから読み込める場合とリソースをネットワークから取得しなければならない場合の判断方法について取り扱います。

HTTP プロトコルにおけるキャッシュの働き

HTTP でキャッシュが行われる一般的な流れは次のようになります。クライアント(通常はブラウザ)が "HTTP GET" で最初のリソースのリクエストを行います。この要求時には、一般的にはキャッシュ情報は送られません。サーバーは "HTTP 200 OK" のメッセージと共にデータを返します。この返信時に、クライアント側がキャッシュを制御するためのヘッダをいくつか追加します。これについて見ていきましょう。

有効期限:

サーバーがクライアントのリクエストに応答する際には、そのリソースがディスクにキャッシュ可能かどうかの情報と、可能な場合には、クライアントがそのリソースを次に読み込む際にどのくらいの期間キャッシュからの読み込みが有効なのかについての情報を送ります。別の言い方をすると、サーバーはクライアントにキャッシュ内のリソースがいつ失効し、再度ネットワークから読み込む必要があるかを伝えます。サーバーやプロキシによって送られる有効期限の情報に関する HTTP のヘッダは以下の通りです(不完全ですが)。

  • Expires: サーバーはクライアントにそのリソースが失効する日時を伝えます。例: “Expires: Fri, 29 Apr 2011 09:22:59 GMT
  • Cache-Control: max-age: サーバーはクライアントにリソースの寿命を伝えます。つまり、そのリソースが生きていると考えることができる期間です。例: サーバーがクライアントにそのリソースが1時間(=3600秒)キャッシュすることができることを伝える場合: “Cache-Control: max-age=3600
    例:
  • Cache-Control: s-maxage: max-age と同様ですが、共有キャッシュ(キャッシュプロキシ等)のために使用されます。プライベートキャッシュ(ブラウザのキャッシュ等)では無視されます。これは max-age がプライベートキャッシュに使われているからです。例: サーバーが間にあるプロキシに対してそのリソースが1時間(=3600秒)キャッシュすることができることを伝える場合: “Cache-Control: s-maxage=3600
  • Cache-Control: must-revalidate: 有効期限の情報が不十分なため、サーバーがクライアントにこのリソースを毎回リロードするように伝える場合に使用されます。例えば、クライアントが失効した(古くなった)リソースでもキャッシュから読み込むことを許可されている場合(下記の QNetworkRequest::PreferCache を参照)、“Cache-Control: max-age=0” の指定では十分ではありません。“must-revalidate” の指定によりクライアントがいつでも必ずサーバー自体(間のプロキシでもなく)からリロードすることを保証します。例えば、Facebook や Twitter はこれを最初のページで使用しています(ただし、最初のページから参照される要素では通常は使用されていません)。
    例:
  • Age: リソースの年齢を表します。このヘッダは大本のサーバーでそのリソースが生成された時点からの時間を秒で表します。サーバーからの応答は常に 0 歳であるべきなのでこれは一見冗長に見えますが、応答が直接大本のサーバーから直接受け取るのではなく、プロキシを通すことがしばしばあります(例えば qt.nokia.com でチェックしてみてください)。この場合、この “Age” ヘッダはリソースが大本のサーバーから取得された時点からの秒数を表します。“Age” は “max-age” を計算するために必要とされます。

更新情報:

ローカルキャッシュにリソースを保持している場合、クライアントはサーバーにそのリソースが更新されているかを確認することが可能です。これはサーバーへの問い合わせを常に伴いますが、サーバーがクライアントにそのリソースを最後に取得してから変更がないことを伝えることでデータを節約することができます。この場合、サーバーは本体の内容を送らずに、ヘッダだけの HTTP メッセージを送ります。更新情報を送るために使われる HTTP ヘッダは以下の通りです(不完全ですが)。

サーバーから:

  • Last-Modified: サーバーがクライアントに最後にそのリソースが更新された日時を伝えます。
  • ETag: サーバーがそのリソースのバージョン識別子を送ります。これはデータ本体のハッシュ関数とみなすことができ、リソースが変更された場合にはこれも変わるでしょう。

クライアントから:

  • If-Modified-Since: クライアントがサーバーに指定した日時以降にリソースが更新されている場合のみデータを送るように伝えます。例えば Last-Modified ヘッダが変更された場合が該当します。もし変更されていなければ、サーバーは "HTTP 304 Not Modified" メッセージを送ります。このメッセージには HTTP ヘッダのみを含み、ボディは含まれません。もし変更されていれば、サーバーは "HTTP 200 OK" メッセージをボディと共に送ります。
    例:
  • If-None-Match: クライアントがサーバーに新しいバージョンの識別子、例えば ETag ヘッダが変更されていた場合にのみデータを送るように伝えます。変更がない場合には、サーバーは "HTTP 304 Not Modified" メッセージを送ります。このメッセージには HTTP ヘッダのみを含み、ボディは含まれません。もし変更されていれば、サーバーは "HTTP 200 OK" メッセージをボディと共に送ります。
    例:

面白いことに、日時を含めるヘッダ (“Expires“、“Last-Modified“、“If-Modified-Since“)は HTTP 1.0 の時点で既に規定されていました。HTTP 1.1 では日時よりも、クライアント時刻に対する相対的な時間のデータ(“max-age“、“s-maxage“)や、バージョン情報(“ETag“、“If-None-Match“)に重点を置いています。これは日時を正確に扱うにはサーバーとクライアントの時刻が同期している必要があるからです。ETag や相対的な時間のデータではこれらの時刻が同期している必要がありません。ここで取り上げたすべてのヘッダは現在でも広く使われています。

キャッシュが Qt で動く仕組み

デフォルトでは [qt QNetworkAccessManager] クラスを使用して HTTP でリソースを取得する際にはディスクキャッシュは利用されません。キャッシュを有効にするには、[qt QNetworkDiskCache] クラスか [qt QAbstractNetworkCache] の派生クラスを実装し、生成したインスタンスを [qt "setCache()" l=qnetworkaccessmanager m=#setCache] で QNetworkAccessManager のインスタンスに設定してください。

これにより、Qt はリソースが有効な場合にはキャッシュから読み込み、そうでない場合にはネットワークから取得するようになります。この際、上記で触れたような更新情報を可能な限り活用します。

Qt がリソースをネットワークから取得する振る舞いは、QNetworkRequest::CacheLoadControlAttribute を [qt QNetworkRequest setAttribute] で設定することによって調整が可能です。

  • AlwaysNetwork: 常にサーバーから取得し、“Cache-Control: no-cache” と “Pragma: no-cache“ の設定により中間キャッシュのリロードも強制する。
  • PreferNetwork (デフォルト): リソースはキャッシュされ、キャッシュされたリソースの年齢が(“Age“、“Cache-Control: max-age“ のヘッダで与えられる)寿命に達していない場合と(“Expires“ ヘッダで受け取る)失効日時に達していない場合にはキャッシュから読み込まれます。リソースが失効していたり寿命に達していた場合はサーバーから読み込みます。この際、(“Last-Modified” の場合は “If-Modified-Since”を、“ETag” の場合は “If-None-Match” による)更新情報が付加されます。
  • PreferCache: リソースがキャッシュにあり有効期限内の場合にはキャッシュから読み込みます。PreferNetwork とは対照的に、(寿命を過ぎているなどで)期限切れの場合でもキャッシュから読み込みます。リソースが(“Expires” ヘッダの情報で与えられる期限が)失効している場合やキャッシュ内にない場合には PreferNetwork と同様の動作になります。
  • AlwaysCache: 利用可能な場合にはキャッシュから取得します。ネットワークにはアクセスしません。オフラインモードとしてよく登場します。キャッシュが見つからない場合にはエラーになります。

さらにキャッシュの制御をしたい場合には、[qt QNetworkRequest setRawHeader] で(“Cache-Control: max-age” や “Cache-Control: max-stale“ などの)ヘッダを追加してください。

Qt で改善すべき点

  • 有効性判定の実装: リソースが失効日を持たず(“Expires” ヘッダがない場合)、ページの年齢が決められない場合(“max-age” か “Age” ヘッダがない場合)、クライアント側でそのページが有効かどうかの判定を実装できるようにする仕組み。特に、リソースが “Last-Modified” ヘッダを持つ場合、現在までの時間の割合(HTTP RFC では 10%)によってリソースが有効だとみなすことができます。例えば、リソースの “Last-Modified” が10日前の場合、1日は有効だとみなすことができます。
  • 寿命の計算 は再作業が必要です。
  • キャッシュからの リソースの取得 の高速化
  • HTTP Vary ヘッダを考慮すべきです。

いつも通り、上記に対する投票やコメントをお願いします。