このところ暖かい日が続いているなあと思っていたら、急に寒くなって喉の調子を悪くしました。
ところで最近、なぜかSocketからサーバを作る必要があってコードを書いたので、自分の中の整理も含め、JavaでTCPサーバを実装する方法についてまとめたいと思います。
Java SE 7において、サーバソケットを扱う方法は3つあります。
最初は基本ということで、私が高校生だった頃には既に活躍していた、普通のソケット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(); }
順序としてはこうです。
早速実行してみましょう。
$ 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を使ってみることにしましょう。