Stack Overflow

前回のnioのsocket readがbusy loopになってしまう問題について、Stack Overflowでも同じような事象が出ていました。

Java Selector returns SelectionKey with OP_READ without data in infinity loop after writing to channel

And, as I said selector returns SelectionKey for reading without any data in SocketChannel and read() returns -1. That is a problem, becasue select() not block and load CPU. This happens only when I write() to channel, before it’s working as needed. ? befire Oct 29 ‘11 at 11:33

また、Nettyの実装に置き換えたところうまくいった、という記述もありました。

I’ve replaced implementation of unblocking sokcets with Netty framework (all works good). But still interested in solution for this problem.

Netty

ということで、githubのNetty(4.0.0.Alpha1-SNAPSHOT)を使って問題が解消されるか確認することにしました。

netty/netty - github

まず、git cloneして全てのモジュールを作成し、ローカルリポジトリにインストールした後、mvn eclipse:eclipseでEclipseに取り込みました。

その後、netty-sampleにあるEchoServerの実装を動かしてみたところ、busy loopは起こりませんでした。確かにNettyの実装を使うことで回避できることが確認できました。

前回の実装でbusy loopの原因を確認するべく、Nettyの実装を少し見てみました。すると、netty-transportのio.netty.channel.socket.nio.NioWorkerのreadメソッドの中に、以下のような記述がありました。

1if (ret < 0 || failure) {
2    k.cancel(); // Some JDK implementations run into an infinite loop without this.
3    close(channel, succeededFuture(channel));
4    return false;
5}

retはSocketChannel#readの戻り値で読み込んだバイト数です。この値がEOS(-1)の場合はSelectionKey#cancel()を呼び出し、channelをcloseしており、コメントにはinfinite loop対応という文字が。

Canceled SelectionKey on EOS

ということで、早速前回のコードに適用して動かしてみました。

 1package net.wrap_trap.example.nio;
 2
 3import java.io.IOException;
 4import java.net.InetSocketAddress;
 5import java.nio.ByteBuffer;
 6import java.nio.channels.SelectionKey;
 7import java.nio.channels.Selector;
 8import java.nio.channels.ServerSocketChannel;
 9import java.nio.channels.SocketChannel;
10import java.util.Iterator;
11import java.util.Set;
12
13public class EchoServer {
14
15    private ServerSocketChannel serverChannel;
16    private Selector selector;
17
18    public EchoServer() throws IOException {
19        serverChannel = ServerSocketChannel.open();
20        serverChannel.socket().setReuseAddress(true);
21        serverChannel.socket().bind(new InetSocketAddress(8000));
22
23        serverChannel.configureBlocking(false);
24        selector = Selector.open();
25        serverChannel.register(selector, SelectionKey.OP_ACCEPT);
26    }
27
28    public void start() throws IOException {
29        System.out.println("start");
30        try {
31            while (selector.select() > 0) {
32                System.out.println("select loop");
33                Set<SelectionKey> keys = selector.selectedKeys();
34                for (Iterator<SelectionKey> it = keys.iterator(); it.hasNext();) {
35                    SelectionKey key = it.next();
36                    it.remove();
37
38                    if (key.isAcceptable()) {
39                        System.out.println("accept");
40                        SocketChannel channel = serverChannel.accept();
41                        channel.configureBlocking(false);
42                        channel.register(key.selector(), SelectionKey.OP_READ);
43                    } else {
44                        SocketChannel channel = (SocketChannel) key.channel();
45                        if (key.isReadable()) {
46                            System.out.println("read");
47                            ByteBuffer buffer = ByteBuffer.allocate(4096);
48                            int len = channel.read(buffer);
49                            if (len > 0) {
50                                buffer.flip();
51                                channel.write(buffer);
52                            } else if (len == -1) {
53                                key.cancel();
54                                channel.close();
55                            }
56                        }
57                    }
58                }
59            }
60        } finally {
61            for (SelectionKey key : selector.keys()) {
62                try {
63                    key.channel().close();
64                } catch (IOException ignore) {
65                    ignore.printStackTrace();
66                }
67            }
68        }
69    }
70
71    public static void main(String[] args) throws IOException {
72        EchoServer echoServer = new EchoServer();
73        echoServer.start();
74    }
75}

上記コードを動かしてみたところ、busy loopを回避することができました。また、1本のsocketで複数回やり取りすることもできています。

Conclusion

この結果より、read時にEOSを検知したらSelectionKey#cancel()を呼び出すことで、busy loopを回避できることが分かりました。

まだ気になっている点は、read時にOP_WRITEを指定することなくSocketChannel#write()を呼び出しても問題なくクライアントに応答を返せていることです。一旦nioの本をちゃんと読んでみようかな。

そしてNetty。コードを少し読んでみた限りですが、nioの嵌りどころをうまく回避するような実装が入っており、間違いなくnioを直接扱うよりも楽です。nioで嵌るようなことがあればNettyを使ってみましょう。