概要

TCP ソケット通信は HTTP や SMTP といった上位プロトコルの基盤であり、ネットワークプログラミングの出発点です。Java では ServerSocket でサーバーを待ち受け、Socket でクライアントから接続するという明確な構造が用意されています。この記事では、最もシンプルなエコーサーバーを例に、TCP の接続確立からメッセージの送受信、切断までの一連の流れを実装します。Java 8 では匿名クラスと明示的な型宣言で書いていた処理が、Java 17 では record や var で簡潔になり、Java 21 では仮想スレッドで接続ごとのスレッド管理から解放されるという進化も確認できます。

使いどころ

社内ツール間のプロセス間通信を TCP ソケットで実装し、コマンドを送受信する

既存のテキストベースプロトコル(POP3、FTP など)のクライアントを Java で実装する

テスト用のモックサーバーを ServerSocket で構築し、クライアント側の通信処理を検証する

コード例

TCP エコーサーバーとクライアント
import java.io.*;

public class TcpSocketSample {

    record ClientMessage(String text) {}

    public static void startEchoServer(int port, ExecutorService executor)
            throws IOException {
        var serverSocket = new ServerSocket(port);
        executor.submit(() -> {
            try {
                var client = serverSocket.accept();
                try (var in = new BufferedReader(
                         new InputStreamReader(client.getInputStream()));
                     var 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) {

            }
        });
    }

    public static void runClient(int port) throws IOException {
        var messages = List.of(
            new ClientMessage("Hello, Server!"),
            new ClientMessage("EXIT")
        );
        try (var socket = new Socket("localhost", port);
             var out = new PrintWriter(socket.getOutputStream(), true);
             var in = new BufferedReader(
                 new InputStreamReader(socket.getInputStream()))) {
            for (var msg : messages) {
                out.println(msg.text());
                System.out.println("受信: " + in.readLine());
            }
        }
    }

    public static void main(String[] args) throws Exception {
        int port = 9000;
        var executor = Executors.newSingleThreadExecutor();
        startEchoServer(port, executor);
        Thread.sleep(100);
        runClient(port);
        executor.shutdown();
    }
}

Java 8 / 17 / 21 の完全なサンプルコードは GitHub リポジトリ で確認できます。

Version Coverage

record でメッセージを型安全に表現し、var で記述を簡潔にできる。テキストブロックも活用可能。

Java 17
// Java 17: record + var で簡潔に
record ClientMessage(String text) {}
var messages = List.of(
    new ClientMessage("Hello"),
    new ClientMessage("EXIT"));
try (var socket = new Socket("localhost", port);
     var out = new PrintWriter(
         socket.getOutputStream(), true);
     var in = new BufferedReader(
         new InputStreamReader(
             socket.getInputStream()))) {
    for (var msg : messages) {
        out.println(msg.text());
        System.out.println(in.readLine());
    }
}

Library Comparison

標準 API(ServerSocket/Socket)TCP 通信の仕組み学習や、テスト用モックサーバーの構築。依存なしで動作する。プロトコル解析やフレーム管理は全て自前。本番用途には向かない。
Netty高性能な非同期 TCP サーバーを構築する場合。チャネルパイプラインでプロトコル処理を組み立てられる。学習コストが高い。小規模な用途にはオーバースペック。
gRPC / Protocol Buffersマイクロサービス間の型安全な RPC 通信が必要な場合。TCP の生の通信を理解する目的には合わない。プロトコル定義ファイルの管理が必要。

注意点

ServerSocket.accept() はブロッキング呼び出し。メインスレッドで呼ぶと他の処理が止まるため、別スレッドで実行すること

クライアント側の Socket は try-with-resources で確実にクローズする。クローズし忘れるとポート枯渇の原因になる

BufferedReader.readLine() は改行文字を待つ。送信側が println() ではなく print() を使うと受信側が永久にブロックする

ループバック(localhost)では動作するが、異なるマシン間ではファイアウォールやポート開放の設定が必要

FAQ

TCP と UDP の使い分けはどうすればよいですか。

信頼性が必要な通信(ファイル転送、API 呼び出し)は TCP、速度優先で多少のパケットロスが許容される場合(ログ配信、ストリーミング)は UDP を選びます。

同時に複数クライアントを処理するにはどうしますか。

accept() で受け取った Socket を ExecutorService に渡してスレッドプールで処理します。Java 21 なら Virtual Thread で接続ごとにスレッドを生成できます。

ソケット通信のデバッグはどうすればよいですか。

Wireshark でパケットキャプチャするか、telnet / nc コマンドで手動接続してプロトコルの動作を確認するのが効果的です。

関連書籍

この記事のテーマをさらに深く学びたい方へ。

※ Amazon アソシエイトリンクを含みます