java-recipes

ホーム ネットワーク › N-03: TCP ソケット通信

N-03: TCP ソケット通信(ServerSocket / Socket)

ServerSocket Socket を使った TCP ソケット通信の基本を解説します。TCP は接続確立(3ウェイハンドシェイク)・データ転送・切断という 手順を踏む信頼性の高いプロトコルです。HTTP や FTP など多くのアプリケーション層プロトコルの基盤となっています。

TCP 通信の仕組みと主なクラス

クラス役割主なメソッド
ServerSocketサーバー側。指定ポートで待受し、クライアントの接続を受け付けるaccept()、close()
Socketクライアント側。サーバーへの接続と双方向通信を担うgetInputStream()、getOutputStream()、close()
BufferedReaderテキスト行単位で読み込むラッパーreadLine()
PrintWriterテキスト行単位で書き込むラッパーprintln()

サンプルコード

シンプルなエコーサーバーの実装例です。サーバーはクライアントから受信したメッセージに 「ECHO: 」を付けて返します。「EXIT」を受信するとサーバーを終了します。

Sample.java
import java.io.*;
import java.net.*;
import java.util.concurrent.*;

public class TcpSocketSample {
    // エコーサーバーを別スレッドで起動し、クライアントから接続してメッセージをやり取りするデモ

    // サーバー側: ServerSocket でクライアント待受
    public static void startEchoServer(int port, ExecutorService executor) throws IOException {
        ServerSocket serverSocket = new ServerSocket(port);
        executor.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    Socket client = serverSocket.accept(); // クライアント接続待ち
                    try (BufferedReader in = new BufferedReader(new InputStreamReader(client.getInputStream()));
                         PrintWriter out = new PrintWriter(client.getOutputStream(), true)) {
                        String line;
                        while ((line = in.readLine()) != null) {
                            out.println("ECHO: " + line); // 受信した文字列をそのまま返す
                            if ("EXIT".equals(line)) {
                                break;
                            }
                        }
                    }
                    serverSocket.close();
                } catch (IOException e) {
                    // サーバー終了
                }
            }
        });
    }

    // クライアント側: Socket で接続してメッセージ送受信
    public static void runClient(int port) throws IOException {
        try (Socket socket = new Socket("localhost", port);
             PrintWriter out = new PrintWriter(socket.getOutputStream(), true);
             BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()))) {

            out.println("Hello, Server!"); // メッセージ送信
            System.out.println("受信: " + in.readLine());

            out.println("EXIT");
            System.out.println("受信: " + in.readLine());
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 9000;
        ExecutorService executor = Executors.newSingleThreadExecutor();

        startEchoServer(port, executor);
        Thread.sleep(100); // サーバー起動待ち

        runClient(port);

        executor.shutdown();
    }
}

よくあるミス・注意点

⚠️ ソケットは必ず try-with-resources でクローズする

Socket ServerSocket Closeable を実装しているため、 try-with-resources で自動的にクローズできます。 クローズを忘れると OS のファイルディスクリプタ(FD)が枯渇し、 新しい接続を受け付けられなくなります。

// NG: クローズ漏れでリソースが枯渇する可能性がある
Socket socket = new Socket("localhost", 9000);
// ... 処理 ...
// close() を呼び忘れると FD リークが起きる

// OK: try-with-resources で確実にクローズする
try (Socket socket = new Socket("localhost", 9000)) {
    // ... 処理 ...
} // ブロックを抜けると自動的に socket.close() が呼ばれる

📌 accept() はブロッキング呼び出しなので別スレッドで実行する

serverSocket.accept() はクライアントが接続してくるまで 処理をブロックします。メインスレッドで呼び出すとアプリケーション全体が停止してしまうため、 必ず別スレッド(または ExecutorService)で実行しましょう。

📌 ポート番号は 1024 以上を使う

0〜1023 番ポート(ウェルノウンポート)は OS の管理下にあり、 バインドには管理者権限(root / Administrator)が必要です。 アプリケーションでは 1024〜65535 の範囲のポートを使いましょう。 他のアプリケーションと競合しないよう、使用前に空きポートを確認してください。

📌 Java 21 の仮想スレッドで大量クライアントを効率的に処理できる

従来の OS スレッドは生成コストが高く、数千クライアントの同時接続で スレッド数が増えすぎてメモリ不足になりがちです。 Java 21 の仮想スレッド(Executors.newVirtualThreadPerTaskExecutor())は 軽量で大量生成できるため、1 クライアント 1 スレッドのシンプルな設計を維持しながら 高いスループットを実現できます。

テストする観点

  • サーバーとクライアントが正常に接続でき、メッセージの送受信が行えるか
  • 送信したメッセージに「ECHO: 」が付いて正しく返ってくるか
  • 「EXIT」メッセージを送るとサーバーが正常終了するか
  • 接続を切断した際に readLine()null を返してループが終了するか
  • サーバーが起動していない状態でクライアントが接続しようとすると ConnectException がスローされるか
  • ソケットのタイムアウト(setSoTimeout())を設定した場合に SocketTimeoutException が発生するか

GitHub でソースコードを見る →