Javaアプリケーションを開発・運用する中で、最も頻繁に遭遇するネットワークエラーの一つがjava.net.ConnectException: Connection refusedです。

この例外は、クライアントがサーバーに対して接続を試みたものの、何らかの理由でサーバー側がその接続を拒否した際にスローされます。

一見すると単純なエラーに思えますが、その原因はアプリケーションの設定ミスからネットワークインフラの制限、OSのリソース不足まで多岐にわたります。

本記事では、このエラーが発生するメカニズムを深く掘り下げ、具体的な原因の特定方法と解決策を詳しく解説します。

java.net.ConnectExceptionとは何か

Javaのネットワークプログラミングにおいて、java.net.ConnectExceptionは、リモートホストへの接続を試みた際に発生する例外です。

これはjava.net.SocketExceptionのサブクラスであり、さらにその親クラスはjava.io.IOExceptionです。

この例外がスローされる際、メッセージとしてConnection refused (Connection refused)が表示されます。

これは、パケットが目的のIPアドレスに到達したものの、オペレーティングシステム(OS)レベルで接続が明示的に拒否されたことを示しています。

タイムアウト(Connection timed out)とは異なり、サーバーマシン自体は見つかっているが、入り口で「お断り」されている状態であると理解すると分かりやすいでしょう。

例外の階層構造

Javaにおけるこの例外の位置付けを確認しておきましょう。

java.lang.Object

すべてのクラスのルートクラスです。

java.lang.Throwable

エラーや例外の階層の基底クラスです。

java.lang.Exception

プログラムがキャッチすることを想定した例外クラスの基底です。

java.io.IOException

入出力処理において発生する例外の基底クラスです。

java.net.SocketException

ソケット通信中に発生する例外です。

java.net.ConnectException

ソケット接続時にエラー(接続拒否など)が発生した際にスローされる例外です。

この階層が示す通り、入出力エラーの中でも特にネットワークソケットに関連し、さらに「接続フェーズ」で失敗したことを特定するための例外です。

Connection refusedが発生する主な原因

このエラーが発生する理由は大きく分けて5つあります。

トラブルシューティングを行う際は、以下のいずれかに該当しないか順番に確認していくことが定石です。

1. サーバープロセスが起動していない

最も単純かつ頻繁にある原因は、接続先のサーバープログラムが実行されていないことです。

クライアントが特定のポートに接続しようとしても、そのポートをリッスン(待ち受け)しているプロセスが存在しない場合、OSは即座に接続を拒否します。

2. ポート番号の指定ミス

サーバーは起動しているものの、クライアント側で指定しているポート番号が間違っているケースです。

例えば、サーバーが 8080 番ポートで待機しているのに対し、クライアントが誤って 8000 番ポートに接続しようとすると、このエラーが発生します。

3. リッスンアドレス(バインドアドレス)の設定不備

サーバーがどのネットワークインターフェースで待ち受けているかも重要です。

例えば、サーバーが 127.0.0.1(ループバックアドレス)のみでリッスンしている場合、外部のホストからそのサーバーのIPアドレスを指定して接続しようとしても拒否されます。

0.0.0.0を指定してすべてのインターフェースで待ち受けるか、特定のプライベートIPアドレスを指定する必要があります。

4. ファイアウォールやセキュリティグループによる拒否

ネットワーク経路上のファイアウォールや、OS自体のパケットフィルタリング(iptables, ufw, Windows Firewallなど)が接続をブロックしている場合です。

ただし、ファイアウォールの設定によっては「拒否(REJECT)」ではなく「破棄(DROP)」されることもあり、その場合は Connection refused ではなく ConnectException: Connection timed out になる傾向があります。

明示的に拒否応答を返す設定になっている場合にこのエラーが出現します。

5. サーバーのバックログ(接続キュー)が満杯

サーバーが高い負荷にさらされており、新しい接続要求を処理しきれない場合に発生します。

OSレベルの接続待ち行列(バックログ)の上限に達すると、それ以上の接続要求は拒否されることがあります。

これは高負荷時のスパイクなどでよく見られる現象です。

診断のためのステップとツール

原因を特定するためには、Javaコードを修正する前にネットワークの状態を調査する必要があります。

以下のツールを活用しましょう。

netstat / ss コマンドによる確認

サーバー側で、意図したポートがリッスン状態にあるかを確認します。

Shell
# Linux/macOSで8080番ポートの状態を確認
netstat -an | grep 8080
# または ss コマンドを使用
ss -tuln | grep 8080

出力結果に LISTEN と表示されていれば、プロセスは正しくポートを開いています。

telnet / curl コマンドによる疎通確認

クライアント側の端末から、ネットワーク的に疎通が可能かを確認します。

Shell
# telnetによる確認
telnet 192.168.1.10 8080

# curlによる確認
curl -v http://192.168.1.10:8080

もしこれらのコマンドでも Connection refused が返ってくるのであれば、問題はJavaコードではなく、インフラやサーバーの稼働状態にあることが確定します。

Javaプログラムでの再現と対策

ここでは、実際にこの例外を発生させるコードと、それを適切にハンドリングする方法を見ていきましょう。

エラーを再現するシンプルなクライアント

サーバーが起動していない状態で以下のコードを実行すると、例外が発生します。

Java
import java.io.IOException;
import java.net.Socket;
import java.net.ConnectException;

public class ConnectionRefusedDemo {
    public static void main(String[] args) {
        String host = "localhost";
        int port = 9999; // 起動していないポートを指定

        try {
            System.out.println(host + ":" + port + " に接続を試みます...");
            // ソケットの作成を試みる
            Socket socket = new Socket(host, port);
            
            // 接続に成功した場合(この例では到達しない)
            System.out.println("接続に成功しました!");
            socket.close();
        } catch (ConnectException e) {
            // 接続拒否時のハンドリング
            System.err.println("エラー: サーバーに接続を拒否されました。");
            System.err.println("原因: " + e.getMessage());
            // 実務ではここでリトライ処理やログ出力を行う
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}
実行結果
localhost:9999 に接続を試みます...
エラー: サーバーに接続を拒否されました。
原因: Connection refused (Connection refused)

実践的な解決策:リトライロジックの実装

一時的なネットワークの不安定さや、サーバーの再起動中に発生する ConnectException に対処するためには、指数バックオフ(Exponential Backoff)を用いたリトライロジックの実装が推奨されます。

Java
import java.io.IOException;
import java.net.Socket;
import java.net.ConnectException;

public class RetryConnection {
    private static final int MAX_RETRIES = 3;
    private static final int INITIAL_WAIT = 1000; // 1秒

    public static void main(String[] args) {
        connectWithRetry("localhost", 8080);
    }

    public static void connectWithRetry(String host, int port) {
        int attempt = 0;
        int waitTime = INITIAL_WAIT;

        while (attempt < MAX_RETRIES) {
            try (Socket socket = new Socket(host, port)) {
                System.out.println("接続成功!");
                return;
            } catch (ConnectException e) {
                attempt++;
                System.err.println("接続拒否 (" + attempt + "/" + MAX_RETRIES + ")。 " + waitTime + "ms 後に再試行します。");
                try {
                    Thread.sleep(waitTime);
                } catch (InterruptedException ie) {
                    Thread.currentThread().interrupt();
                    return;
                }
                waitTime *= 2; // 待ち時間を倍にする
            } catch (IOException e) {
                e.printStackTrace();
                break;
            }
        }
        System.err.println("最大リトライ回数に達しました。接続できません。");
    }
}

特定の環境における注意点

現代のアプリケーション開発では、物理マシン上だけでなく、コンテナやクラウド環境特有の原因も考慮する必要があります。

Docker環境でのConnection refused

Dockerコンテナ内で実行されているJavaアプリケーションから、ホストマシンのデータベースなどに接続しようとする際、localhost を指定するとエラーになります。

コンテナにとっての localhostコンテナ自身を指すからです。

解決策

Docker Composeを使用している場合はサービス名を使用するか、ホストを指す特殊なDNS名(例: host.docker.internal)を使用してください。

Kubernetes環境

Pod間の通信において、Serviceのターゲットポートと実際のPodのリッスンポートが一致していない場合に発生します。

また、readinessProbe が通っていないためにトラフィックがルーティングされていないケースもあります。

AWSなどのクラウド環境

セキュリティグループの設定で、インバウンドルールが許可されていない場合です。

前述の通り、多くのクラウドファイアウォールはパケットを「DROP」しますが、一部のネットワーク構成やプロキシ(ALBなど)経由の場合、ターゲットが異常であればプロキシが Connection refused502 Bad Gateway を返します。

サーバー側の設定改善

クライアント側の対策だけでなく、サーバー側でこのエラーを未然に防ぐ設定も重要です。

バインドアドレスの確認

サーバーアプリケーション(Spring BootやTomcatなど)の設定ファイルを確認し、外部からの接続を許可するようになっているかチェックします。

設定項目値の意味
127.0.0.1ローカルループバック。自分自身からしか接続できない。
192.168.x.x特定のネットワークインターフェースでのみ待ち受ける。
0.0.0.0すべてのネットワークインターフェースで待ち受ける。

バックログの調整

Javaの ServerSocket を直接使用している場合、コンストラクタでバックログのサイズを指定できます。

Java
// 第2引数がバックログのサイズ(接続待ち行列の長さ)
ServerSocket serverSocket = new ServerSocket(8080, 500);

デフォルト値は通常 50 ですが、短時間に大量の接続がくるシステムではこの値を増やすことで Connection refused の発生を抑えられる場合があります。

ただし、OS側の制限(somaxconn)も合わせて確認する必要があります。

まとめ

java.net.ConnectException: Connection refused は、プログラムのバグというよりも、システム間の連携や環境設定の問題であるケースがほとんどです。

発生時には慌てず、まず「サーバープロセスが生きているか」「ポート番号は正しいか」「リッスンアドレスは適切か」という基本項目を、netstatcurl といったツールを用いて確認してください。

また、アプリケーションの実装においては、一時的なネットワークエラーに備えたリトライ処理の導入や、エラーログに接続先のホスト名とポート番号を明記するようにしておくことが、迅速なトラブルシューティングの鍵となります。

ネットワークエラーを正しく理解し、堅牢なJavaアプリケーションを構築していきましょう。