Webの世界-通信高速化編-
概要
この記事はWebの世界-HTTPS編-の続きになります。
HTTPの歴史的経緯や、HTTPSの問題点などを顧みるとHTTP/HTTPSの発展の中に通信の高速化というのが大きな目標としてあるのがわかる。
当然、HTTPもTCP/IPやアプリケーションなどの下位/上位レイヤの機能と併用する必要があること、また物理法則的な面でも限界が存在することから手を入れられる範囲は制限されている。
ただ、その中でもなんとかしようとする技術がいくつも存在するため、今回はそれらの技術を説明する。
HTTPの各バージョンでそれぞれ採用しているものが違うため、その点は注意されたい。
高速化技術
コンテンツ圧縮
非常にシンプルな考え方で、HTTPで送信する中身を何かしらの圧縮方式で圧縮して送信する。
コンテンツの圧縮はHTTP通信の初期の段階から考慮されており、以下のような圧縮形式が利用されることがある。
- br
- compress
- deflate
- exi
- gzip
- identity
- pack200-gzip
- sdch
実際に作業するのは主にブラウザやサーバが行うため、ユーザが意識することは多くない。
どの圧縮方式を利用しているかはContent-Encoding
をヘッダーに付与して相手側に伝える。
また、HTTP/2.0からはHPACKという方式でヘッダーの圧縮も行われるようになっている。
キャッシュ
同じコンテンツに複数回アクセスする場合、コンテンツをいくら圧縮しても、リクエストごとに一々サーバからダウンロードしていては効率が良いとはいえない。そこでコンテンツの内容に変化がなければダウンロードを抑制し、見た目の通信速度を向上させることができる。
この場合のキャッシュは基本的にブラウザ側に保存される。
管理方法が複数存在する。以下に示す。
更新日時によるキャッシュ
非常にシンプルな考え方で、HTTP/1.0からのキャッシュの仕組みになる。
サーバはクライアントにコンテンツを返す時にLast-Modified
ヘッダーにコンテンツの更新日時を付与して送る。クライアント側はそれらのURLとコンテンツの情報をキャッシュし、再度同じURLにリクエストする場合はIf-Modified-Since
、もしくはIf-Unmodified-Since
にLast-Modified
ヘッダーの情報を付与してサーバに送る。
サーバ側はそれらのヘッダーに付与された日時情報と、コンテンツの更新日時を比較し、更新日時が日時情報より先になければステータスコードとボディには何も含まないでレスポンスを返す。
そのステータスコードを受け取った時点でクライアント側はリクエストしたコンテンツが更新されていないことがわかるため、キャッシュされたコンテンツを表示する。
なお、If-Modified-Since
はGETもしくはHEADメソッドでのみ利用でき更新日時が日時情報より先になければステータスコード:304を返す。
If-Unmodified-Since
はPOSTで利用でき、更新日時が日時情報より先になければステータスコード:412を返す。
Expiresヘッダー
前述のLast-Modified
ヘッダーによるキャッシュ制御の仕組みでは、キャッシュの利用可否を判断するのにクライアント-サーバ間で通信が発生してしまい、その分だけ見かけの速度が低下する。
そこで通信なしでキャッシュの利用可否を判断するのがExpires
ヘッダーによる管理になる。
Expires
ヘッダーはコンテンツにアクセスした際にサーバから日時を付与して返却される。
クライアントは設定された日時の範囲内であれば、キャッシュされたコンテンツは有効であると判断し、サーバにリクエストを送らず、キャッシュされたコンテンツを返す。
この仕組みで注意する必要があるのは、ヘッダーが設定されている場合は、設定した日時の範囲内の時はサーバにリクエストを投げることさえしないため、日時の範囲と設定するコンテンツを考慮しないと延々とコンテンツが更新されなくなる可能性がある。
Pragmaヘッダー
この機能はHTTP/1.1では後述するCache-Control
に取り込まれているため、現状あまり意味はありません。
前述のキャッシュ管理の機能はいずれもサーバ側からキャッシュ利用の条件を指定していたが、Pragma
ヘッダーを利用することで、クライアントからキャッシュの利用有無を制御することもできる。
Pragma
ヘッダーには唯一no-cache
という値を設定することができる。
この値が設定されている場合、途中のプロキシサーバにキャッシュされたコンテンツが存在していても、プロキシに対してサーバにリクエストを送付させることができる。
HTTPSなどではプロキシサーバがそもそもリクエストの中身を読み取ったりすることはできないため、後方互換性以外の意義はない。
ETag
更新日時ではなく、コンテンツのハッシュ値を比較してキャッシュを管理するのがETagになる。この仕組みはHTTP/1.1から導入された。
サーバがコンテンツを返却する際にETagヘッダにコンテンツのハッシュ値を計算し、付与する。
クライアント側は同じURLにアクセスする際に対応するETagを付与して送信し、サーバ側のハッシュ値と比較し、同じであればステータスコード:304を返却する。
クライアント側は304が帰ってきた場合はキャッシュされているコンテンツを利用する。
Cache-Controlヘッダー
HTTP/1.1で追加されたサーバ側からキャッシュの利用を支持する仕組みになる。
レスポンスのヘッダーにCache-Control
ヘッダーを付与してキャッシュのコントロールができるようになる。なお、Expires
ヘッダーが同時に付与されている場合はCache-Control
が優先される。
値 | 概要 |
---|---|
public | ユーザ間でキャッシュを再利用可能 |
private | ユーザ間でキャッシュ再利用不可能 |
max-age=n | キャッシュの鮮度を設定 |
s-maxage | max-age=nと同じで共有キャッシュに対する設定 |
no-cache | キャッシュが有効か毎回サーバに問い合わせる |
no-store | キャッシュしない |
なお、リクエストのヘッダーにCache-Control
を付与してプロキシサーバに対してキャッシュの取り扱いを指示できるが、あまり利用しないため割愛する。
Keep-Alive
HTTPはTCP/IPの仕組みを利用している。このKeep-AliveはTCP/IPのコネクションの使用方法をうまく制御することで通信の高速化を図る方法である。
TCP/IPレイヤの話であるので詳細は割愛するが、通信を開始する前に、クライアント-サーバ間で通信が可能かどうかコネクションを確立する作業が入る。
これは3ウェイハンドシェイク(three-way handshaking)と呼ばれ、
- syn(クライアント→サーバ)
- ack(サーバ→クライアント)
- fin(クライアント→サーバ)
と1.5往復してから通信を開始する。
また、通常のHTTPでは一回の通信ごとにコネクションが閉じてしまうため、毎回3ウェイハンドシェイクを行う必要がある。
Keep-AliveではConnection: Keep-Alive
というヘッダを付与することで、指定のタイムアウト時間を超えるまでコネクションを接続しっぱなしにすることができる。
結果的にハンドシェイクの頻度が下がるため、通信が高速化する。
パイプライニング
これ自体はあまり実用的ではなく、後述するストリームの基礎となったということが重要であるため、簡単な説明にとどめる。
通常のHTTP通信では1リクエストを送った場合は、1レスポンスが帰ってくるまではクライアント側は何も作業ができない。
そこで、最初のリクエストのレスポンスが帰ってくる前に次々にリクエストを送信し、その待ち時間を無くすのがパイプライニングになる。
ストリーム
パイプライニングを基礎に、HTTP/2.0でより実用的にしたものがストリームになる。
ストリームは一本のTCP接続を仮想的に分割し、複数のストリーム(仮想TCP接続)を生成し、並列でデータのやり取りができるようにする技術である。
ストリームはTCPのように煩雑なハンドシェイクが必要なく、簡単に接続ができる。よって、大量のストリームを接続し、パイプライニングのように同時にいくつものデータをやり取りすることで通信の高速化を実現できる。
UDP
HTTP/3.0(QUIC)では、通信を行うに辺り色々な制約があるTCPではなくUDPを利用して高速な通信を行う。
こちらもHTTPレイヤの話ではなく、まだ普及段階にあるため詳細は解説しないが、UDPではハンドシェイクもコネクションも誤りの検出なども行わず、サーバの負荷を軽減できる。
実際のところはUDPで行わなくなった機能はアプリケーション層で肩代わりすることになるが、通信のオーバーヘッドが減少することが期待される。
まとめ
このレイヤの話になると、TCP/IPなどのより低いレイヤの知識も必要になるためより深く勉強したいが、際限がない気もする…