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()を使用して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を受け取ります。

問題の回避方法

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

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

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