Javaで作るTCPサーバ(1.5)

こんばんは。梅雨に入ったというのに、全然雨が降らないですね。天気がいいのはとりあえず洗濯物にとってはいいことですが、あんまり降らないようだと今度は夏の水不足が心配になってきます。

ところで今日は前回の続きというか補足をします。最初はノンブロッキングAPIを使う方法を書くつもりでしたが、ちょっと気が変わりました(笑)。

ブロッキングモードとノンブロッキングモード

前回も少し触れた通り、ソケット通信を扱う方法として、ブロッキングモードとノンブロッキングモードというものがあります。簡単に言えば通信をする際にいちいちブロックするのがブロッキングモード、ブロックしないのがノンブロッキングモードです。

・・・それではあまり説明になっていないのでもう少し詳しく言いましょう。ネットワーク越しにデータを読み取ろうとしたり書き込もうとしたりする場合、それ相応の遅延が発生します。その遅延時間をどうするのかという違いがブロッキングモードとノンブロッキングモードの違いです。

ブロッキングモード
読み込みや書き込みを完了するまで待ちます。つまりコード上、readやwriteのメソッドをコールすると完了するまで制御はブロックします。
ノンブロッキングモード
「今できる処理だけをする」ことを基本にします。つまりコード上、readやwriteのメソッドをコールすると今読み取れるデータのみをreadし、或は今書き込めるデータのみをwriteして、すぐに制御が戻ります。

前回 Java SE 7 においてサーバソケットを扱う方法は3通りあると言いました。クラス名でいうなら、

  • java.net.ServerSocket
  • java.nio.channels.ServerSocketChannel
  • java.nio.channels.AsynchronousServerSocketChannel

この3通りです。前回使った ServerSocket はブロッキングモードで処理をします。2番目の ServerSocketChannel はブロッキングモードとノンブロッキングモードのどちらでも処理をすることができます。加えて効率の良いChannelクラスを使って読み書きできますので、ブロッキングモードで処理したい場合であってもJDK1.4以上の環境ならば、ServerSocket より ServerSocketChannel を使用することを考えた方がよいと思います。

java.nio.channels.ServerSocketChannel をブロッキングモードで使う

というわけで、前回の ServerSocket を ServerSocketChannel に置き換えて、ブロッキングモードで処理してみましょう。

try (ServerSocketChannel listener = ServerSocketChannel.open();) {
    listener.setOption(StandardSocketOptions.SO_REUSEADDR, Boolean.TRUE);
    listener.bind(new InetSocketAddress(8080));
    System.out.println("Server listening on port 8080...");
    while (true) {
        try (SocketChannel channel = listener.accept();) {
            System.out.printf("ACCEPT %s%n", channel);
            Bytes.copy(channel, channel);
            System.out.printf("CLOSE %s%n", channel);
        }
    }
} catch (IOException e) {
    e.printStackTrace();
}

GitHub 完全なコード

んん? あまり変わっていないような・・・? その通りです。ブロッキングモードで処理する場合、だいたい似たようなコードを書けばよいのです。ちなみに前回説明を省略しましたが、Bytes#copy()というメソッドはJDKには(今のところ)ありません。以下のようなシグネチャのメソッドを想定しています。

Bytes#copy(ReadableByteChannel from, WritableByteChannel to);
Bytes#copy(InputStream from, OutputStream to);

ソケットは読み込みも書き込みもできますので、SocketChannel も ReadableByteChannel と WritableByteChannel を両方実装しているというわけです。

再びスレッド化する

さて上記のコードは前回と同じく1対1で通信するコードになっています。なのでやはりスレッド化したコードも掲げておきます。

ExecutorService worker = Executors.newCachedThreadPool();
try (ServerSocketChannel listener = ServerSocketChannel.open();) {
    listener.setOption(StandardSocketOptions.SO_REUSEADDR, Boolean.TRUE);
    listener.bind(new InetSocketAddress(8081));
    System.out.println("Server listening on port 8081...");
    while (true) {
        final SocketChannel _channel = listener.accept();
        System.out.printf("ACCEPT %s%n", _channel);
        worker.submit(new Runnable() {
            @Override
            public void run() {
                try (SocketChannel channel = _channel;) {
                    Bytes.copy(channel, channel);
                    System.out.printf("CLOSE %s%n", channel);
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        });
    }
} catch (IOException e) {
    e.printStackTrace();
} finally {
    worker.shutdown();
}

GitHub 完全なコード

これもほとんど同じですね。

それでは次回はいよいよ、ServerSocketChannel をノンブロッキングモードで使ってみたいと思います。

そういえば今週はJava EE 7のリリースがあります。私もオンラインのローンチイベントに参加登録しています。昼間は参加が難しそうなので、深夜の部かあるいは、あとでYouTubeを見るとかそんな感じになるかもしれませんが・・・。

コメントを残す