Javaで作るTCPサーバ(1)

このところ暖かい日が続いているなあと思っていたら、急に寒くなって喉の調子を悪くしました。

ところで最近、なぜかSocketからサーバを作る必要があってコードを書いたので、自分の中の整理も含め、JavaでTCPサーバを実装する方法についてまとめたいと思います。

Javaでサーバソケットを扱う

Java SE 7において、サーバソケットを扱う方法は3つあります。

  • まずはいわゆる普通のソケットAPIです。JDK1.0の時代からあるAPIで、(つまりこの旧き良きAPIは1996年から存在します) java.net.ServerSocket を使います。
  • 次に「ノンブロッキング」と呼ばれるモードで処理を行うAPIがあります。java.nio.channels パッケージにある、ServerSocketChannel と Selector を中心に使います。このAPIはJDK1.4で追加されました。
  • そして最後に、非同期で処理を行う最も新しいAPIがあります。これも java.nio.channels パッケージにある、AsynchronousServerSocketChannel を中心に使います。このAPIはJDK1.7で追加されました。

java.net.ServerSocket を使う

最初は基本ということで、私が高校生だった頃には既に活躍していた、普通のソケットAPIを使ってエコーサーバを実装してみましょう。”普通のソケットAPI”と言っても、他のAPIでもソケットの扱い方が違うというだけで、扱うもの自体はあくまで”普通のソケット”には違いないのですが・・・。

これはごくシンプルに書けますので、コードを先に記載します。

try (ServerSocket listener = new ServerSocket();) {
    listener.setReuseAddress(true);
    listener.bind(new InetSocketAddress(8080));
    System.out.println("Server listening on port 8080...");
    while (true) {
        try (Socket socket = listener.accept();) {
            InputStream from = socket.getInputStream();
            OutputStream to = socket.getOutputStream();
            Bytes.copy(from, to);
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

順序としてはこうです。

  1. ServerSocketを作り、ポートにバインドする
  2. クライアントからの接続を待ち受け(つまりこの ServerSocket#accept() をコールした時点で、スレッドがブロックします)、接続があったらACCEPTしてSocketを得る
  3. Socketのストリームを使ってアレコレする
  4. Socketを閉じる
  5. 以降繰り返し

早速実行してみましょう。

$ java SocketServer
Server listening on port 8080...

これでサーバが起動しました。別のターミナルを開いて接続してみます。

$ telnet localhost 8080
Trying ::1...
Connected to localhost.
Escape character is '^]'.

接続できました。何か入力すれば、入力したものがそのまま出力されてくるはずです。Congratulations! ・・・ただ、このサーバには1つ大きな問題点があります。

スレッド化する

その問題点とは、複数のクライアントからの接続を同時に処理できないということです。ためしに複数のターミナルから接続してみると、1つのクライアントの接続を閉じてからでないと次のクライアントの処理がされないことがわかります。

それというのも、先述のコードではSocketを1つずつしか処理していないからですね。従ってそこをスレッド化しましょう。

ExecutorService worker = Executors.newCachedThreadPool();
try (ServerSocket listener = new ServerSocket();) {
    listener.setReuseAddress(true);
    listener.bind(new InetSocketAddress(8081));
    System.out.println("Server listening on port 8081...");
    while (true) {
        final Socket socket = listener.accept();
        worker.submit(new Runnable() {
            @Override
            public void run() {
                try {
                    InputStream from = socket.getInputStream();
                    OutputStream to = socket.getOutputStream();
                    Bytes.copy(from, to);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        socket.close();
                    } catch (IOException e) {
                    }
                }
            }
        });
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    worker.shutdown();
}

これで複数の接続を同時に処理できる、極めて実用的なエコーサーバを手にすることができました。

しかしちょっと待ってください、サーバソケットを扱う方法が他にも2つ、あったのでした。それらにはこの”普通のソケットAPI”にはない利点があります(同時に欠点もあるのですが)。次回は2つ目のAPIであるノンブロッキングAPIを使ってみることにしましょう。

コメントを残す