持続接続

HTTPの持続接続とは

HTTPキープアライブとも呼ばれるHTTPの持続接続またはHTTPの接続再利用では、それぞれの要求と応答の単一のペアについて新しい接続を開く場合とは対照的に、HTTP要求/応答の複数のペアを送信または受信する際に同一のTCP接続を使用しています。持続接続を使用することは、HTTPパフォーマンスを向上させるうえで非常に重要な点です。

持続接続を使用するメリットには、次のものがあります。

SSL/TLS経由でHTTPSまたはHTTPを使用することにより、メリットがさらに明確となります。そのような状況では、持続接続により、TCP接続の初期設定に加えて、セキュリティの関連付けを確立するための損失の大きいSSL/TLSハンドシェークの回数が減少します。

HTTP/1.1では、どの接続においても持続接続がデフォルトの動作となります。つまり、指示がないかぎり、サーバーからのエラー応答のあとであっても、クライアントはサーバーが持続接続を保持するものとみなします。ただし、プロトコルは、クライアントとサーバーがTCP接続を閉じることを信号で知らせるようにする手段を提供します。

接続を再利用できるようにするには

TCPはその性質上、ストリーム・ベースのプロトコルであるため、既存の接続を再利用するには、HTTPプロトコルに前の応答の最後と次の応答の始めを示す手法を設定する必要があります。つまり、これは、接続中に送られるすべてのメッセージは、メッセージの長さ(接続終了時に定義されない)をそれぞれ独自に定義する必要があるということです。自己の境界設定は、Content-Lengthヘッダーを設定するか、エンティティ本体をエンコードしてチャンク単位に区切って転送することにより達成されます。なお各チャンクは特定のサイズで開始され、応答本体は指定の最後のチャンクで終了します。

中間にプロキシ・サーバーが存在する場合の処理

持続接続は1つのトランスポート・リンクのみに適用されるため、プロキシ・サーバーで、そのクライアントおよびオリジン・サーバー(または別のプロキシ・サーバーに対して)との永続的/非永続的な接続を個別に正しく合図することが重要です。HTTPクライアントまたはサーバーの観点から見ると、持続接続に関するかぎり、プロキシ・サーバーの存在の有無は透過的となります。

現在のJDKで実施するキープアライブの処理

JDKでは、HTTP/1.1とHTTP/1.0の両方の持続接続をサポートします。

アプリケーションで応答本体の読取りが終了したとき、またはURLConnection.getInputStream()により返されるInputStreamのclose()がアプリケーションによって呼び出されるときは、JDKのHTTPプロトコル・ハンドラでは、接続をクリーンアップしようとし、成功した場合は、将来のHTTP要求による再利用を見越してその接続を接続キャッシュに送ります。

HTTPキープアライブのサポートは透過的に行われます。ただし、その制御は、システム・プロパティhttp.keepAlivehttp.maxConnectionsのほか、HTTP/1.1で規定されている要求ヘッダーや応答ヘッダーによって行うことができます。

キープアライブの動作を制御するシステム・プロパティは次のとおりです。

http.keepAlive=<boolean>
default: true

キープアライブ(持続)接続をサポートすべきかどうかを示します。

http.maxConnections=<int>
default: 5

任意の時点で接続先ごとにキープアライブを実行できる最大接続数を示します。

接続の持続性に影響を与えるHTTPヘッダーは次のとおりです。

Connection: close

要求ヘッダーまたは応答ヘッダーのどちらかのフィールドで値「close」を使用して「Connection」ヘッダーの指定を行なった場合、現在の要求/応答が完了したあとは、接続が「持続的」だとみなされるべきではないことを示しています。

現在の実装では、応答本体はバッファに格納されません。これは、接続の再利用を目的として、アプリケーションで応答本体の読取りを終了する必要があるか、またはclose()を呼び出して応答本体の残りを破棄する必要性があることを意味しています。さらに、現在の実装では、接続がクリーンアップされる場合にブロック単位の読取りを試みません。これは、応答本体の全体が利用できない場合は接続が再利用されないことを意味します。

JDK 5の新機能 

アプリケーションはHTTP 400または500応答を検出したときに、IOExceptionを無視して、別のHTTP要求を発行することがあります。この場合、ベースとなるTCP接続はキープアライブされません。これは、応答本体が、消費されるように引き続き存在し続けるからです。このため、ソケット接続はクリアされず、再利用不可能となります。アプリケーションでは、IOExceptionをキャッチしたらHttpURLConnection.getErrorStream()を呼び出して応答本体を読み取り、その後にストリームを閉じる必要があります。ただし、既存のアプリケーションによってはこの処理を行わないものもあります。このため、このようなアプリケーションは持続接続からメリットが得られません。この問題に対処するための回避方法を導入しました。

この回避方法では、応答が400以上の場合に制限時間付きで一定量の応答本体をバッファに入れます。このようにすることで、配下のソケット接続を再利用のために解放できます。この背景となっている根拠は、サーバーが400以上のエラーで応答するとき(クライアント・エラーまたはサーバー・エラー、一例は「404: File Not Found」エラー)、サーバーは、通常、コンタクト先と復元に必要な処理を通知するために小規模な応答本体を送信します。

サーバーからのエラー応答後に 接続のクリーンアップに役立つよう、Oracle JDKの実装に固有の新しいプロパティがいくつか導入されています。

主なプロパティは次のとおりです。

sun.net.http.errorstream.enableBuffering=<boolean>
default: false

上記のシステム・プロパティにtrue (デフォルトはfalse)を設定している状態で、応答コードが400以上の場合、HTTPハンドラでは、応答本体のバッファリングを試みます。その結果、配下のソケット接続が解放されて、再利用できるになります。したがって、アプリケーションでgetErrorStream()を呼び出さずに応答本体を読み取り、そのあとにclose()を呼び出す場合でも、配下のソケット接続はキープアライブが有効になり再利用できるようになります。

次の2つのシステム・プロパティで、エラー・ストリームのバッファリングの動作について詳細な制御を行います。

sun.net.http.errorstream.timeout=<int> in millisecond
default: 300 millisecond

sun.net.http.errorstream.bufferSize=<int> in bytes
default: 4096 bytes

推奨されるキープアライブの使用方法

応答本体を無視して接続を破棄しないでください。TCP接続はそのためにアイドル状態になる場合があります。この接続は参照されなくなったときに、ガベージ・コレクトする必要があります。

getInputStream()が正常に返される場合は、応答本体の全体を読み取ります。

HttpURLConnectionからgetInputStream()を呼び出してIOExceptionが発生した場合は、例外をキャッチし、getErrorStream()を呼び出して応答本体を取得します(応答本体が存在する場合)。

応答の内容自体を必要としない場合でも、応答本体を読み取ると接続がクリーンアップされます。ただし、応答本体が長く、先頭部分を確認したあとに残り部分が必要なくなった場合はInputStreamを閉じることができます。ただし、データがさらに続いていることを認識しておく必要があります。そのために、接続はクリアされず、再利用できない場合があります。

次に、上記の推奨事項に準拠したコード例を示します。

try {
        URL a = new URL(args[0]);
        URLConnection urlc = a.openConnection();
        is = conn.getInputStream();
        int ret = 0;
        while ((ret = is.read(buf)) > 0) {
          processBuf(buf);
        }
        // close the inputstream
        is.close();
} catch (IOException e) {
        try {
                respCode = ((HttpURLConnection)conn).getResponseCode();
                es = ((HttpURLConnection)conn).getErrorStream();
                int ret = 0;
                // read the response body
                while ((ret = es.read(buf)) > 0) {
                        processBuf(buf);
                }
                // close the errorstream
                es.close();
        } catch(IOException ex) {
                // deal with the exception
        }
}

応答本体を必要としないことが前もってわかっている場合は、GET要求の代わりにHEAD要求を発行するようにしてください。たとえば、Webリソースのメタ情報のみが必要な場合や、有効期間、アクセス可能性、最新の変更などを確認する場合などです。次にコード例を示します。

URL a = new URL(args[0]);
URLConnection urlc = a.openConnection();
HttpURLConnection httpc = (HttpURLConnection)urlc;
// only interested in the length of the resource
httpc.setRequestMethod("HEAD");
int len = httpc.getContentLength();

JDK6での 変更点

JDK 6より前では、 アプリケーションでHTTP InputStreamを閉じたときに読取り対象のデータが残っていて、この量が少量でなかった場合、キャッシュせずに接続を閉じなければなりませんでした。JDK 6では、 バックグラウンドのスレッドの接続から最大512Kバイトを読み取るため、接続の再利用が可能となります。読み取るデータの正確な量を構成するには、http.KeepAlive.remainingDataシステム・プロパティを使用します。

Copyright © 1993, 2020, Oracle and/or its affiliates. All rights reserved.