HTTP Connections

この頁では、HTTP/1.1における最も重要な技術の一つである「持続的接続」について、そもそもどういう技術なのか、またその実現のための手法についても解説します。

持続的接続

HTTPにおける『接続』とは

RFC 2616のsection 8は、“Connections”、すなわち「接続」について記述されている章です。 この章には、HTTP/1.1における最も重要な技術の一つ、持続的接続{Persistent Connections}について記述されているのですが、本節ではその前に、まず「HTTPにおいて、『接続』とはどういう意味なのか?」について記述いたします。 まずは、section 8.1.1をご覧下さい。

(※) “Persistent Connections”は、「永続的接続」と訳される場合もありますが、当サイトでは「持続的接続」で統一しています。

持続的接続が無い時代には、別々の URL からリソースを取得するために、それぞれで TCP 接続を確立していたため、サーバのロードを増加させ、インターネットの混雑を引き起こす原因になっていた。 インラインイメージやその他の関連するデータの使用のために、クライアントはしばしば短時間に同じサーバへ複数のリクエストを行う必要がある。 これらのパフォーマンスの問題の解析や持続的接続のプロトタイプの実装から得られた結果については、現在入手可能である [26] [30]

HTTPにおける『接続』状態とは、一言でいうと「クライアント-サーバ間でTCP接続が確立されている状態」です。 そして、そのようなTCP接続を確立するプログラムのことをクライアントと呼びます。

初期のHTTPクライアントは、HTTP通信を行うごとにTCP接続を確立していました。 たとえば、あるHTMLの中に9つのインラインイメージ(<img>による画像の呼び出し)を含むようなWebページがあるとします。 この時、HTMLファイルと各画像ファイルに対するリクエスト-レスポンスはそれぞれ別に行う必要があるので、Webページの描画を完成させるためには、計10回の通信を行う必要があります。 しかし、TCP接続においては「接続を確立する」という作業(3ウェイハンドシェイク)が、最も負荷をかける処理なので、埋め込まれたファイルが多ければ多いほど、ページの表示完了まで大きく時間がかかり、またそのようなページが増えることによって、サーバに大きな負荷を与える原因となってしまうのです。

持続的接続による利点

上記のようなデメリットを解消するために、TCP接続を一度確立した後は、その接続を閉じず、その接続を再利用してリクエスト/レスポンスを送ろうとする、「持続的接続」HTTP通信という方式が考え出されました。 この方式によるメリットについても、RFC 2616のsection 8.1.1に記述されています。

持続的 HTTP 接続には、いくつかの利点がある。

HTTP 実装は持続的接続を実装すべきである

単に持続的接続を利用するだけでも上記のような利点があるのですが、HTTP/1.1ではその他にパイプラインという概念も提案されました。 パイプラインについてはRFC 2616のsection 8.1.2.2に記述されています。

持続的接続をサポートするクライアントは、そのリクエストを "パイプライン" する事ができる (例えば、複数のレスポンスを待つ事無く、複数のリクエストを送る)。 サーバは、リクエストが受信されたのと同じ順番で、それらのリクエストのレスポンスを返さなければならない

持続的接続を想定し、接続確立の後にすぐパイプラインを行うクライアントは、もし最初のパイプライン化への試行が失敗した場合は、それらの接続を再試行する準備をすべきである。 クライアントがそのような再試行を行う場合は、その接続が持続的であると分かるまではパイプラインを行ってはならない。 もし、サーバがすべての通信のレスポンスを返す前に接続を閉じてしまったら、クライアントはそれらのリクエストを再送信する準備をしなければならない

クライアントは、冪等でない{non-idempotent} メソッド、あるいはメソッドのシーケンスを使ったリクエストをパイプラインすべきではない (section 9.1.2 参照)。 そうでなければ、転送接続の早期異常終了が不確定な結果を招く事になるだろう。 冪等でない{non-idempotent} リクエストを送ろうとするクライアントは、前のリクエストのレスポンスステータスを受け取るまでリクエストを送る事を待つべきである

これまでのHTTPリクエストでは、一度リクエストを発行したら、それに対するレスポンスを受信するまで、次のリクエストを発行できませんでした。 これに対し、パイプラインでは、そのレスポンスを受信することなく、複数のHTTPリクエストを発行することができます。 これによって、処理時間の短縮や、ネットワーク帯域におけるパケット数の節約が期待できます。

パイプライン中に接続が切断された場合は、リクエストを再発行することになりますが、パイプラインリクエストは持続的接続に依存するので、これが確認されるまではパイプラインを行ってはいけません。 また、冪等でないリクエスト、すなわちレスポンスがリクエストの結果に依存するようなPOSTリクエストの時には、パイプラインを行うべきではありません。

持続的接続の運用上の問題

HTTP/1.1アプリケーションとの持続的接続について

RFC 2616のsection 8.1.4では、HTTP接続における(技術的問題というよりもむしろ)実用上・礼儀上の問題について議論されています。 そもそも持続的接続は、「ネットワーク帯域の節約」を目的に開発された技術であり、この節の議論はすべてそれに関連した話題になっています。

多くのサーバは、もはや接続を維持しないとするタイムアウト値を持っているであろう。 クライアントはたぶん同じサーバへとより多くの接続を持とうとするはずなので、プロクシサーバはサーバが設定するタイムアウト値よりも大きな値にしたほうがよい。 持続的接続の使用は、クライアントやサーバのためのこのタイムアウト値の長さ (あるいは存在) に必要性を置かない。

クライアントやサーバがタイムアウトを望む時は、転送接続上礼儀正しい切断を発行すべきである。 クライアントもサーバも、他方の転送の切断を絶えず監視し、適切にそれに応じるべきである。 もし、クライアントやサーバが、相手側の切断を即座に検出しなければ、それはネットワーク上の不必要なリソース消耗を引き起こすかもしれない。

ほとんどのサーバでは、一定時間接続がないとその接続を切断するようなタイムアウト値を持っています。

(※) たとえば、ApacheというWebサーバの場合、初期値はすべて“httpd.conf”という設定ファイルに記述されますが、そのうち「接続を確立してから最初のリクエストを発行するまでの時間」はTimeout、また「パイプラインを利用して、既に確立されたコネクションから次のリクエストを受け付けるまでの時間」はKeepAliveTimeoutにて設定される値になります。

クライアント、サーバ、あるいはプロクシは、どんな時でも転送接続を切断する事ができる。 例えば、サーバが "アイドル" 状態の接続を切断しようと決めたのと同時に、クライアントは新しいリクエストを送り始めるかもしれない。 サーバ側から見れば接続はアイドルである間に切断されているが、クライアント側から見ればリクエストは進行中である。

これは、クライアント、サーバ、プロクシが、非同期の切断状態{event} から回復できなければならないという事を意味する。 クライアントソフトウェアは転送接続を再度オープンし、リクエストシーケンスが冪等{idempotent} (section 9.1.2 参照) である限り、ユーザインタラクションなしに中止されたリクエストのシーケンスを再送信すべきである。 そうでないメソッドやシーケンスは自動的に再試行してはならないが、ユーザエージェントは人間のオペレータにリクエストの再試行についての選択を尋ねてもよい。 アプリケーションが意味を理解した上で、ユーザ自身の確認の代わりにユーザエージェントソフトウェアが確認してもよい。 この自動再試行は、もし二回目のリクエストシーケンスが失敗したなら繰り返すべきではない

サーバは、もし完全に可能なら、常に一つの接続につき少なくとも一つのリクエストにレスポンスすべきである。 サーバは、ネットワークやクライアントの失敗を疑う場合以外は、レスポンスの転送中に接続を切断すべきではない

全ての HTTP アプリケーションは、いつでも、相手に確認する事なしに接続を終了する事ができます。 従って、もしかしたら、一方が HTTP 通信を行おうとする際に、もう一方が接続を切断してしまうかもしれません。

もし、クライアントがリクエスト送信中にサーバから接続を切断された場合、そのメソッドが冪等である、すなわち「常に同じ結果が返される」事が期待されるメソッドの場合、クライアントの後ろにいる人間ユーザに確認せずに、そのリクエストを再試行する事ができます。 しかし、そうでない場合は (ダイアログを表示する等して) 一度人間ユーザに確認をする必要があります。

サーバは、このような問題を起こさないために、リクエストが発生したら直ちにレスポンスを返すようにすべきです。 そして、(クライアントが通信不可能であるという確かな証拠がない限り) レスポンスの転送中に接続を切断すべきではありません。

持続的接続を使用するクライアントは、サーバへ維持する同時接続の数を制限すべきである。 シングルユーザクライアントは、どんなサーバやプロクシへも 2 接続より多く維持すべきではない。 プロクシは、N は同時のアクティブユーザの数として、別のサーバやプロクシへの接続使用数を多くても 2*N までとすべきである。 これらのガイドラインは HTTP レスポンスタイムを改善し、ネットワークの混雑を避けようとするものである。

例えば、先述の「HTML 中に 9 つのインラインイメージを含む Web ページ」があったとして、現在の殆どのブラウザは(パイプラインではなく)複数の HTTP 接続を同時に開き、並列処理で画像を取得するという手法を取っています。

(※) Web 初期の Mosaic のような非常に古いブラウザでは、これらの TCP 接続を順番に処理していました。そのため、そのパフォーマンスは非常に悪いものでした。

並列処理は、そのユーザにとっては快適性が増すものですが、一方でユーザ間でネットワーク帯域幅(この場合は「TCP 接続数」の意)に差が生まれ、不公平が生じる可能性があります。 また、同時に TCP 接続を 10 も開始するという事は、ネットワークやサーバに多大な負荷をかける行為に他なりません。

サーバは、不特定多数から接続される共有のリソースです。 従って、クライアントは、サーバへ同時に接続する数を多くても 2 までに制限すべきです。 プロクシの場合は、同時に接続されるユーザ数が例えば 50 の場合は、多くても 2 * 50 = 100 までにすべきです。

最近、一部では「高速化」と称して、ブラウザの同時接続値を大きな値にすることで、一度に大量のリソースを取得する行為が行われているようですが、このような行為は「一部の人間によるリソースの占有」とみなされるだけでなく、あまりに長い間続くとサービス拒否(DoS)攻撃とみなされる可能性もあるので、厳に慎むべきです。

HTTP/1.0アプリケーションとの持続的接続について

通信相手のHTTPバージョンが1.0以下である場合、その接続が持続的接続である事を期待してはいけません。 持続的接続には多くのメリットがある事が既に述べた通りであり、これを実現させたいと思うかもしれませんが、HTTP/1.0 においては持続的接続の実装が不完全であり、特にプロクシが介在する場合はその処理はより複雑になるので、その使用には十分な注意が必要です。 RFC 2616 の section 14.10 及び 19.6.2 をご覧下さい。

Connection ヘッダを含む HTTP/1.0 (あるいはそれ以前の) メッセージを受けとったシステムは、このフィールド内の各 connection-token ごとに、connection-token と同じ名前を持つヘッダフィールドをそのメッセージを削除し、無視しなければならない。 これは、HTTP/1.1 以前のプロクシによってそのようなヘッダフィールドを転送するような誤りを起こさないようにするものである。 section 19.6.2 参照。

クライアントやサーバの中には HTTP/1.0 のクライアントやサーバ中における持続的接続についてある以前の実装と互換性を持たせたいと思うかもしれない。 HTTP/1.0 における持続的接続はそれらが既定の振る舞いでは無いとして明確にネゴシエートされる。 HTTP/1.0 の持続的接続の実験的な実装には欠点があり、HTTP/1.1 における新しい機能はこれらの問題を改善するために設計されている。 問題はある既存の 1.0 クライアントが Connection を理解できないプロクシサーバへ Keep-Alive が送っているかもしれず、さらにインバウンドサーバに誤ってそれを転送するかもしれず、そうなると Keep-Alive 接続が確立され、その結果レスポンスでの切断を待っている HTTP/1.0 プロキシのハングアップを引き起こす事になるであろう。 これは HTTP/1.0 クライアントがプロクシと通信する時に Keep-Alive を使用する事を防がなければならないという事である。

しかし、プロクシとの通信は持続的接続の最も重要な使い方であり、その禁止は明らかに受け入れる事はできない。 故に、我々は Connection を無視する古いプロクシと通信する時に使用しても安全な、持続的接続が望まれている事を示すための他のメカニズムを必要とする。 持続的接続は HTTP/1.1 メッセージの既定であり、我々は非持続性を宣言するための新しいキーワード (Connection: close) を導入する。 section 14.10 参照。

持続的接続の元の HTTP/1.0 の形式 (Connection: Keep-AliveKeep-Alive ヘッダ) は RFC 2068 中に記述されている。

具体的な Connection: Keep-AliveKeep-Alive ヘッダについての解説は、RFC 2068 の section 19.7.1 に記述されています。

以下に、持続的接続のオリジナル HTTP/1.0 形式を示す。

HTTP クライアントは、オリジンサーバと接続している時に現在の connection-token へ追加して Keep-Alive コネクショントークンを送る事ができる

 Connection: Keep-Alive

HTTP/1.0 サーバは Keep-Alive コネクショントークンで応答し、クライアントは HTTP/1.0 (あるいは Keep-Alive) 持続的接続を続ける事ができるであろう。

HTTP/1.1 サーバも Keep-Alive コネクショントークンを受け取った上で、HTTP/1.0 クライアントと持続的接続を確立する事ができる。 しかし、HTTP/1.0 クライアントとの持続的接続ではチャンク転送コーディングが使用できないので、それぞれのメッセージの終了境界をマークするための Content-Length を使用しなければならない

クライアントは、Connection ヘッダフィールドを解析するための HTTP/1.1 の規定を守らない HTTP/1.0 プロクシサーバのようなプロクシサーバに Keep-Alive コネクショントークンを送ってはならない

Connection ヘッダは、仕様外のホップバイホップヘッダを転送するための機能も持っているわけですが、これは本来 HTTP/1.0 の仕様には含まれてはいないホップバイホップヘッダである Keep-Alive ヘッダを用いて持続的接続を実現しようという狙いがあるのです。 但し、当然の事ながらサーバ側は Keep-Alive ヘッダの意図を把握していなければいけません。 Keep-Alive ヘッダにはどんな値も指定可能ですが、省略可能でもあり、その場合はサーバで設定される初期値が使用されます。

現在使用されている HTTP/1.1 ブラウザの多くは、Connection: Keep-Alive を送信しています。 既に述べたとおり、HTTP/1.1 では Connection ヘッダを特に指定しなくても接続は常に持続的なのですが、世の中には未だ HTTP/1.0 サーバが存在し、またクライアント側も通信前にそのようなサーバを判別する事はできません。 そこで、持続的接続を行いたい HTTP/1.1 クライアントは前もって Connection: Keep-Alive を送信しておく事によって、HTTP/1.1 サーバとはもちろん、持続的接続に対応した HTTP/1.0 サーバとも持続的接続を実現する事ができるのです。

なお、この Keep-Alive ヘッダについては、RFC 2616 によって obsolete された RFC 2068 の section 19.7.1.1 中に記述されています。

Keep-Alive コネクショントークンがリクエストやレスポンスで伝えられた時、Keep-Alive ヘッダフィールドも含まれるかもしれないKeep-Alive ヘッダフィールドは以下のような形式を取る。

 Keep-Alive-header = "Keep-Alive" ":" 0# keepalive-param

 keepalive-param = param-name "=" value

Keep-Alive ヘッダ自身はオプションであり、パラメータが送られた時のみに使用される。 HTTP/1.1 ではどんなパラメータも定義しない。

もし、Keep-Alive ヘッダが送られたならば、相当するコネクショントークンが伝えられなければならない。 コネクショントークン無しに Keep-Alive ヘッダが受信されたら、Keep-Alive ヘッダは無視されなければならない

但し、現在の多くのブラウザでは Keep-Alive ヘッダを使用していません。 一部のブラウザでは Keep-Alive ヘッダに接続維持時間を指定していますが、RFC 2068 形式は使用していません。

HTTP/1.0プロクシとの持続的接続について

今まで見てきたように、クライアントとオリジンサーバが直接接続している場合は何とかなるということがわかりました。 しかし、この間にプロクシが介在すると、途端に問題は複雑になるのです。 8.1.3をご覧下さい。

プロクシが section 14.10 で指定されるように Connection ヘッダの機能を正確に実装する事は特に重要である。

プロクシサーバは、自身が接続しているクライアントとオリジンサーバ (あるいは別のプロクシサーバ) のそれぞれに持続的接続を知らせなければならない。 それぞれの持続的接続は、一つの転送リンクのみで適用される。

プロクシサーバは、HTTP/1.0 クライアントと HTTP/1.1 の持続的接続を確立してはならない (但し、多くの HTTP/1.0 クライアントに実装されている Keep-Alive ヘッダを使った問題の情報と議論については RFC 2068 参照)。

まず、以下のような連鎖 c1, c2 を考えます。 但し、ユーザエージェント UA とオリジンサーバ O は持続的接続を理解できますが、プロクシ P は持続的接続を理解できないとします。

    request chain -------------------------------->
 UA ----------c1---------- P ----------c2---------- O
    <--------------------------------response chain

ここで、UA が P に Connection: Keep-Alive を送ったとすると、P は Connection: Keep-Alive を理解できないのでこれを素通ししてしまいます。 UA と P の間の接続 c1 は、UA が持続的接続を期待していますので、接続は切れません。 一方、O は Connection: Keep-Alive を受け、持続的接続のために接続 c2 を切らずに待っています。 この結果、この接続は破綻します。

この対策として、Netscape 社はプロクシを介して持続的接続を実現させたい場合に Connection: Keep-Alive の代わりに Proxy-Connection: Keep-Alive を使う事にしました。 UA が P に Proxy-Connection: Keep-Alive を送るとすると、P は Connection: Keep-Alive の時と同様に Proxy-Connection: Keep-Alive を素通しします。 しかし、O には Connection: Keep-Alive が届いてはいないので、持続的接続にはなりません。 従って、c2 は切断されます。 これは HTTP/1.0 の通常の動作と同じものです。

一方、もし P が Proxy-Connection: Keep-Alive を理解できるプロクシである場合、P は受信した Proxy-Connection: Keep-Alive を削除し、代わりに Connection: Keep-Alive を O に送信します。 この結果、O は Connection: Keep-Alive を受け取るので、持続的接続が成立します。

但し、この方法でもプロクシが二つ以上介在する場合だと、接続が破綻する場合があります。 このような場合は、持続的接続に対応したプロクシにアップデートするという方法が一番確実となるでしょう。

参照文献

Webリソース

書籍