持続接続

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() を呼び出して応答本体の残りを破棄する必要性があることを意味しています。さらに、現在の実装では、接続がクリーンアップされる場合にブロック単位の読み込みを行おうとはしません。これは、応答本体がまったく機能しない場合に接続が再利用されないことを意味します。

Tiger の新機能

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

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

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

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

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();

Java SE 6 における変更点

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