持続接続

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>
デフォルト: true

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

http.maxConnections=<int>
デフォルト: 5

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

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

Connection:close

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

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

Tiger の新機能

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

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

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

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

sun.net.http.errorstream.enableBuffering=<boolean>
デフォルト: false

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

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

sun.net.http.errorstream.timeout=<int> (ミリ秒単位)
デフォルト: 300 ミリ秒

sun.net.http.errorstream.bufferSize=<int> (バイト単位)
デフォルト: 4096 バイト

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

応答本体を無視して接続を破棄しないでください。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();

Java SE 6 における変更点

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