Java における正常型と放棄型の接続解放



このドキュメントでは、次のトピックについて説明します。

概要

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() を使用して両方のアプリケーションで接続を終了した場合は、正常型解放になるはずです。では、何が問題なのでしょうか。

問題は、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 を受け取ります。

問題の回避方法

この問題による影響を回避する簡単な方法はいろいろあります。

  1. 高次のプロトコルでデータを構成します。TCP の最上位にある HTTP などのプロトコルでこの問題に対応するには、いつソケットを安全に閉じることができるかを両側で明らかにします。
  2. ソケットを閉じる前に、ソケットからのデータを必ず消費します。こうすると、前述の 2 番目のシナリオを回避できます。
  3. shutdownOutput() を使用します。このメソッドは、そのピアが送信を終了したことを示すために FIN を送信するという点は close() と同じです。しかし、リモートピアが FIN を送信してストリームから end-of-file が読み取られるまでは、ソケットからデータを読み取ることが可能です。次に Socket.close() を使用して、ソケットを閉じることができます。