このドキュメントでは、次のトピックについて説明します。
TCP接続を正常に解放することを保証するのは、TCPネットワーク・アプリケーションの課題です。特別な注意を払わないと、放棄型の解放になります。Javaでは、予期しない放棄型解放は、ソケットへの読取りまたは書込み時にアプリケーションがjava.net.SocketException
を受け取ることでわかります。通常、read()
およびwrite()
はそれぞれ受信または送信したバイト数を示す数値を返します。かわりに例外を受け取った場合は、接続が中断され、データが失われたか破棄された可能性があるということです。このドキュメントでは、ソケット接続が中断される原因と、そのような状況を回避するためのヒントを説明します。ただし、アプリケーションが接続を中断しようとする場合については扱いません。
まず、放棄型と正常型の接続解放について、その違いを認識する必要があります。この違いを理解するために、TCPプロトコル・レベルで行われることを説明します。半独立関係にある、実際には別々の2つのデータ・ストリームとして確立されたTCP接続を考えてみます。2つのピアをAとBとしたとき、一方のストリームではデータをAからBに配信し、もう一方のストリームではBからAに配信するとします。正常型解放は、2つの局面で行われます。まず一方(A)がデータ送信の停止を決め、Bに対してFIN
メッセージを送信します。B側のTCPスタックがFIN
を受信すると、BではAからはこれ以上データがこないと判断します。Bではこれまでのデータをソケットからすべて読み取ると常に、それ以後の読取りではend-of-fileを示す値-1
を返します。この手順は、接続の片方だけが閉じるため、TCP半閉鎖(half-close)と呼ばれます。もう一方側の手順も、まったく同じです。BはFIN
メッセージをAに送信します。BはAが送信したこれまでのデータをソケットからすべて読み取ると、最後に-1
を受信します。
これと対照的に、放棄型閉鎖では、RST (Reset)メッセージを使用します。どちらかがRSTを発行すると、接続全体が中断され、双方のアプリケーションが送信または受信せずにキューに残っているデータはTCPスタックによって破棄されます。
それでは、Javaアプリケーションではどのようにして正常型と放棄型の解放を実行するのでしょうか。まず、放棄型解放から考えます。元のBSDソケットがあるころから存在していた慣習では、「linger」ソケット・オプションは放棄型接続解放を強制実行するために使用できます。どちらのアプリケーションでもSocket.setLinger
(true, 0)を呼び出し、このソケットが閉じると放棄型(RST)の手順が使用されることをTCPスタックに通知できます。lingerオプションを設定しても、すぐに影響はありません。ただし、設定後にSocket.close()
が呼び出されると、接続は中断してRSTメッセージが発行されます。後述するとおり、接続を中断する方法は他にもありますが、この方法がもっとも簡単です。
close()
メソッドは、放棄型だけでなく、正常型解放の実行にも使用されます。つまり、もっとも簡単な方法では、正常型解放と放棄型解放の違いは、Socket.close()
を呼び出す前に、前述のようにlinger(0)オプションを設定しないことだけです。接続された2つのピアAとBの例で考えてみます。AがSocket.close()
を呼び出すと、AからBにFIN
メッセージが送信されます。BがSocket.close()
を呼び出したときは、BからAにFIN
が送信されます。実際はlingerオプションのデフォルトの設定では、放棄型閉鎖を使用しないことになっているため、Socket.close()
を使用して2つのアプリケーションで接続を終了した場合は、正常型解放になるはずです。では、何が問題なのでしょうか。
問題は、Socket.close()
のセマンティックスと、TCP FIN
メッセージの間にあるわずかな不一致です。TCP FIN
メッセージを送信することは「送信を完了しました」という意味で、Socket.close()
は「送信と受信を完了しました」という意味です。Socket.close()
を呼び出したときは、データを送信できなくなるだけでなく、データの受信もできなくなります。では、たとえばAがソケットを閉じて正常型解放を試みたのに、Bがデータを送信し続けると何が起こるでしょうか。これは、TCP仕様によって問題なく許可されます。TCPでは接続の一方が閉じたことだけが考慮されるためです。しかし、Aのソケットが閉じているため、Bが送信し続けてもデータを読み取る相手がいません。このときAのTCPスタックは、強制的に接続を終了するためにRSTを送信する必要があります。
よくある別のシナリオとして、予期しないSocketException
が発生する場合は次のとおりです。AがBにデータを送信したのに、Bはデータをすべて読み取らずにソケットを閉じたとします。このときBのTCPスタックは、データが実際に失われたことを認識し、正常型のFIN
手順を使用するのではなくRSTを発行して、強制的に終了します。Aは、ソケットでデータを送信または受信しようとした場合に、SocketException
を受け取ります。
この問題による影響を回避する簡単な方法はいろいろあります。
shutdownOutput()
を使用します。このメソッドは、そのピアが送信を終了したことを示すためにFIN
を送信するという点はclose()
と同じです。しかし、リモート・ピアがFIN
を送信してストリームからend-of-fileが読み取られるまでは、ソケットからデータを読み取ることが可能です。次にSocket.close()
を使用して、ソケットを閉じることができます。