Asynchronous
前回ノンブロッキングI/Oを使ってみましたが、今回はNIO2の非同期I/Oを試してみます。
NIO2の非同期I/Oは、大きく2つの方法が提供されています。
- CompletionHandler
- Future
今回は非同期I/OのCompletionHandlerを使ってecho serverを作ってみました。
CompletionHandler
CompletionHandlerは、I/O操作時にコールバックとして登録しておくクラスのインターフェースです。I/O操作が成功した場合はこのインターフェースのcompletedが、失敗した場合はfailedが呼び出されます。
CompletionHandlerは2つのパラメータを取ります。1つ目が戻り値の型、2つ目がI/O操作時にattachするオブジェクトのクラスとなります。
今回の例では、accept/read/writeの際にこのインターフェースをimplementしたクラスをコールバックとして登録しています。read/writeについては入出力を行うAsynchronousSocketChannelのオブジェクトをattachしています。acceptの戻り値はAynchronousSocketChannel、read/writeの戻り値はIntegerとなる為、各パラメータは以下のようになります。
- Acceptor implements CompletionHandler
- Reader implements CompletionHandler
- Writer implements CompletionHandler
Implementation
CompletionHandlerを使用してecho serverを書いてみました。accept/read/writeの各処理のコールバックはそれぞれクラスを分けており、その分コードも長くなってますが、もちろん無名クラスで書くことも可能です。このあたりはJavaScriptでコールバックを登録するお馴染みの書き方に似ていることもあって、そんなに違和感がありません。
1package net.wrap_trap.example.nio;
2
3import java.io.IOException;
4import java.net.InetSocketAddress;
5import java.net.StandardSocketOptions;
6import java.nio.ByteBuffer;
7import java.nio.channels.AsynchronousServerSocketChannel;
8import java.nio.channels.AsynchronousSocketChannel;
9import java.nio.channels.CompletionHandler;
10
11public class AsyncEchoServer {
12
13 private AsynchronousServerSocketChannel serverChannel;
14
15 public void start() throws IOException {
16 System.out.println(String.format("start: name: %s", Thread.currentThread().getName()));
17 serverChannel = AsynchronousServerSocketChannel.open();
18 serverChannel.setOption(StandardSocketOptions.SO_REUSEADDR, true);
19 serverChannel.bind(new InetSocketAddress(8000));
20 serverChannel.accept(serverChannel, new Acceptor());
21 }
22
23 class Acceptor implements CompletionHandler<AsynchronousSocketChannel, AsynchronousServerSocketChannel> {
24
25 private final ByteBuffer buffer = ByteBuffer.allocate(1024);
26
27 public Acceptor(){
28 System.out.println("an acceptor has created.");
29 }
30
31 public void completed(final AsynchronousSocketChannel channel, AsynchronousServerSocketChannel serverChannel) {
32 System.out.println(String.format("write: name: %s", Thread.currentThread().getName()));
33 channel.read(buffer, channel, new Reader(buffer));
34 serverChannel.accept(serverChannel, new Acceptor());
35 }
36
37 public void failed(Throwable exception, AsynchronousServerSocketChannel serverChannel) {
38 throw new RuntimeException(exception);
39 }
40 }
41
42 class Reader implements CompletionHandler<Integer, AsynchronousSocketChannel> {
43
44 private ByteBuffer buffer;
45
46 public Reader(ByteBuffer buffer){
47 this.buffer = buffer;
48 }
49
50 public void completed(Integer result, AsynchronousSocketChannel channel){
51 System.out.println(String.format("read: name: %s", Thread.currentThread().getName()));
52 if(result != null && result < 0){
53 try{
54 channel.close();
55 return;
56 }catch(IOException ignore){}
57 }
58 buffer.flip();
59 channel.write(buffer, channel, new Writer(buffer));
60 }
61 public void failed(Throwable exception, AsynchronousSocketChannel channel){
62 throw new RuntimeException(exception);
63 }
64 }
65
66 class Writer implements CompletionHandler<Integer, AsynchronousSocketChannel> {
67
68 private ByteBuffer buffer;
69
70 public Writer(ByteBuffer buffer){
71 this.buffer = buffer;
72 }
73
74 public void completed(Integer result, AsynchronousSocketChannel channel) {
75 System.out.println(String.format("write: name: %s", Thread.currentThread().getName()));
76 buffer.clear();
77 channel.read(buffer, channel, new Reader(buffer));
78 }
79
80 public void failed(Throwable exception, AsynchronousSocketChannel channel) {
81 throw new RuntimeException(exception);
82 }
83 }
84
85 public static void main(String[] args) throws IOException, InterruptedException{
86 new AsyncEchoServer().start();
87 while(true){
88 Thread.sleep(1000L);
89 }
90 }
91}
Acceptor#completedの最後にAsynchronousSocketChannel#acceptを呼び出しているのは、2つめ以降の接続を受け付ける為です。Writer#completedの最後にAynchronousSocketChannel#readを呼び出しているのは、クライアントから1つの接続につき2回以上のsendを受け付ける為です。
Thread
このecho serverを実行すると、標準出力に以下のように出力されます。
start: name: main
an acceptor has created.
accept: name: Thread-3
an acceptor has created.
read: name: Thread-2
write: name: Thread-1
read: name: Thread-1
write: name: Thread-2
read: name: Thread-2
acceot/read/writeのコールバックは、mainスレッド以外のスレッドで実行されています。つまり非同期処理はスレッドを使って実現されていることになります。
上記結果を見る限り、1リクエストあたり最大3スレッド使用しており、スレッド自体が消費するリソースとコンテキストスイッチの回数が気になるところです。また、accept/read/write間で値を受け渡す際にthread local領域は使えないことになります。
Conclusion
今回はNIO2の非同期I/OのCompletionHandlerを使ってecho serverを書いて見ました。ノンブロッキングI/Oの時と比較すると、大分扱いやすいコードになっています。
CompletionHandlerを使用した場合、非同期I/Oの実現にはスレッドが使用されていることから、同時接続数がかなり多い場合にサーバのリソースを使ってしまうのではないかという懸念があります。