HOWTO · Java

Java.Net.SocketTimeoutException: 接続タイムアウト

今日の記事では、java.net.SocketTimeoutException: Connection timed out について説明します。

このページの内容

java.net.SocketTimeoutException: Connection timed out は、ネットワーク操作が許可された時間内に完了しなかったことを意味します。クライアント接続の開始時に発生した場合は、接続先のホスト、ポート、経路、待ち受け状態、設定されたタイムアウトを確認してください。このメッセージだけでは、どれが原因かは特定できません。Java や基盤のネットワークスタックは、接続失敗を別の例外型やメッセージで報告することもあります。そのため、メッセージだけでなく、操作の種類と接続先も調べてください。

java.net.SocketTimeoutException: Connection timed out の意味

TCP ソケットは、アプリケーション間の接続における一方のエンドポイントを表します。クライアントは java.net.Socket を使ってサーバーの java.net.ServerSocket に接続します。サーバーが接続を受け入れると、そのクライアントとの通信に使う別のソケットが作成されます。タイムアウトは、該当する操作が待機する時間を制限します。ネットワーク経路を修復したり、利用できないサービスに接続できるようにしたりするものではありません。

クライアントが Socket.connect(SocketAddress, timeout) を呼び出すと、正のタイムアウト値をミリ秒で指定することで接続試行の待機時間を制限できます。接続が完了する前に時間切れになると、Java はタイムアウトを報告します。この設定はソケットの読み取りタイムアウトとは異なります。接続に成功しても、その後の読み取りでデータを長時間待つことがあります。

接続タイムアウト、読み取りタイムアウト、accept タイムアウトを区別する

処理がどこで待機しているかに応じて設定を選びます。

  • 接続タイムアウトは、クライアントが外向きの接続を確立している間に適用されます。タイムアウトを指定して Socket.connect を使います。
  • 読み取りタイムアウトは、接続済みのソケットが受信データを待つ間に適用されます。Socket.setSoTimeout で設定します。接続の確立時間は制限しません。
  • accept タイムアウトは、サーバーがクライアントからの接続を待つ間に適用されます。ServerSocket.setSoTimeout で設定します。クライアントの外向き接続がタイムアウトするかどうかには影響しません。

これらの制限はそれぞれ独立しています。時間を制限したい操作ごとに設定し、想定されるネットワークやサービスの応答時間に合う値を選んでください。これらのソケット API では、タイムアウトに 0 を指定すると無期限に待機します。有限の待機時間を設定したい場合は 0 を使わないでください。

よくある原因と診断方法

考えられる原因は仮説として扱い、Java プロセスが実行されているマシンとネットワーク環境から確認してください。

  1. タイムアウトが短すぎる。 通常の接続遅延やアプリケーションの遅延予算と比べてください。遅くても接続できる状況が想定される場合に限り、値を大きくします。上限を大きくすると、失敗が判明するまでの時間も長くなります。
  2. ホスト名またはポートが誤っている、あるいはサービスが待ち受けていない。 設定されたホスト名、名前解決後のアドレス、ポート、サーバーの待ち受け状態を確認してください。サービスがクライアントから到達可能なアドレスにバインドされているかも確認します。
  3. ネットワーク経路が利用できない。 クライアント環境から DNS の名前解決とルーティングを確認してください。該当する場合は、コンテナーやクラウドのネットワーク設定も調べます。
  4. ファイアウォールまたは接続先のポリシーが通信を遮断している。 同じクライアントネットワークからの接続と、許可されたネットワークからの接続を比較し、関連するファイアウォール規則やサービスログを確認してください。
  5. サーバーの応答が遅い、停止している、または過負荷になっている。 接続を試みた時刻前後の稼働状況とログを確認してください。接続タイムアウトだけでは、サーバーが正常であることも、タイムアウトだけが問題であることも証明できません。

接続拒否と接続タイムアウトは、異なるネットワーク上の結果を示す場合があります。ただし、メッセージは OS やネットワーク経路によって異なります。設定を変更する前に、接続先、操作、経過時間、設定されたタイムアウト、元の例外の詳細を記録してください。

クライアントソケットに接続タイムアウトを設定する

未接続の Socket を作成し、接続先と正の有限なタイムアウトを指定して Socket.connect を呼び出します。基本となる呼び出しは Socket client = new Socket(); と client.connect(new InetSocketAddress(host, port), 5000); です。使用後にソケットが閉じるよう try-with-resources を使い、診断ログにはホストとポートを記録してください。

この自己完結型の例では、一時ポートで待ち受けるローカルサーバーにクライアントが接続します。接続タイムアウトは 1 秒です。サーバーはクライアントを 1 つ受け入れ、ブロックの終了時に両方のソケットが閉じます。コードと出力に含まれる表示文はバイト単位で保持されるため、英語のままです。

import java.net.ServerSocket;
import java.net.Socket;

public class LocalConnect {
    public static void main(String[] args) throws Throwable {
        try (ServerSocket server = new ServerSocket(0)) {
            Thread worker = new Thread(() -> {
                try {
                    server.accept().close();
                } catch (Throwable ignored) {
                }
            });
            worker.start();
            try (Socket client = new Socket()) {
                client.connect(new java.net.InetSocketAddress("127.0.0.1", server.getLocalPort()), 1000);
                System.out.println("Connected to local server");
            }
            worker.join();
        }
    }
}
Connected to local server

提供された実行では終了コード 0 となり、コードに示されている英語の行が出力されました。このローカルでの成功は API の使用例を示すもので、リモートの本番ホストに到達できることを証明するものではありません。出力ブロックの表示文も変更されず、英語のままです。

サーバーソケットに accept タイムアウトを設定する

ServerSocket.setSoTimeout を正のミリ秒値で呼び出してから accept() を実行します。たとえば server.setSoTimeout(10000); とすると、server.accept() がクライアントを待つ時間を制限できます。その時間内にクライアントが到着しなければ、待機中の accept 操作は SocketTimeoutException で終了します。サーバーソケット自体は引き続き使用できるため、サーバーのループでタイムアウトを捕捉して処理を続けたり、待機状態を報告したりできます。処理が終わったら、受け入れたクライアントソケットと待ち受け用サーバーソケットを閉じてください。この設定は、受け入れた接続を維持できる時間や、その接続で読み取りが待機する時間を制限しません。

この単独の例では、接続するクライアントを意図的に用意していません。accept の待機期限が切れる様子と、その後も待ち受けソケットが開いたままであることを確認します。コードと出力に含まれる表示文はバイト単位で保持されるため、英語のままです。

import java.net.ServerSocket;
import java.net.SocketTimeoutException;

public class AcceptDeadline {
    public static void main(String[] args) throws Exception {
        try (ServerSocket server = new ServerSocket(0)) {
            server.setSoTimeout(100);
            try {
                server.accept();
                System.out.println("Unexpected client connection");
            } catch (SocketTimeoutException timeout) {
                System.out.println("Accept deadline reached");
            }
            System.out.println("Listening socket open: " + !server.isClosed());
        }
    }
}
Accept deadline reached
Listening socket open: true

提供された実行では終了コード 0 となり、コードに示されている英語の 2 行が出力されました。出力ブロックの表示文も変更されず、英語のままです。

HttpURLConnection にタイムアウトを設定する

URL 接続を HttpURLConnection として開く場合、接続の確立を制限するには HttpURLConnection.setConnectTimeout を、応答データの待機を制限するには setReadTimeout を呼び出します。たとえば url.openConnection() を HttpURLConnection にキャストした後、接続や読み取りを行う前に connection.setConnectTimeout(8000); と connection.setReadTimeout(10000); を設定します。値の単位はミリ秒で、0 はタイムアウトなしを意味します。応答を読み取り、そのストリームを try-with-resources で閉じてから、リクエストの完了時に disconnect() を呼び出してください。タイムアウト固有の処理には SocketTimeoutException を捕捉し、より広いネットワークエラーにはその他の IOException を捕捉します。クラス名は正確に HttpURLConnection です。

SocketTimeoutException を処理し、修正を確認する

タイムアウトする可能性がある操作の周囲で SocketTimeoutException を捕捉し、ホスト、ポート、操作、設定された上限、経過時間などの有用な情報をログに記録してください。無期限に再試行してはいけません。操作を繰り返しても安全な場合に限り、再試行回数を制限し、各試行がリクエスト全体の期限内に収まるようにします。失敗した試行でリソースが漏れないよう、ソケットとストリームは finally ブロックまたは try-with-resources で閉じてください。

変更後は、失敗したときと同じ実行環境から確認します。アドレスとポートを確認し、サーバーが待ち受けていて到達可能かを調べたうえで、接続の確立、読み取り、accept のうち該当する操作を実行してください。実測時間を設定値と比べ、サーバー側のログも確認します。タイムアウトを大きくしても同じ失敗が遅れて起きるだけなら、適切な上限に戻し、代わりにルーティング、通信の遮断、待ち受け状態、サービスの稼働状況を調べてください。ローカルサーバーへの接続テストに成功しても、リモートの接続先や本番ネットワークに到達できるとは限りません。