Recent Posts

RSS Feeds

Grizzlyの概要 2 : Java New I/Oで実装されたサーバ


さてさて、今日も引き続きGrizzlyを説明したいと思いますが、
その前に、やはりJava New I/Oについて理解して頂かなければ
本当の意味でGrizzlyを理解していただけませんので、Grizzlyの
ソースコードを追う前にJava New I/Oについて簡単に復習して
おきたいと思います。
ただし、ここではほんの概要程度の説明ですのでちゃんと
理解されたい方は別途Java NIOについて説明されている記事を
読んで理解してください。

※ 本エントリの一番最後にJava New I/Oを学ぶことができる
  参考URLを示しています。

Java New I/Oでノンブロッキングサーバの実装:

前回、Java New I/Oを使って実装されたサーバは、今までの
マルチスレッド型のBlocking I/Oを使用して実装されたサーバより
新規スレッドを作成する必要がなくスレッド数を少なくすることが
できると説明しましたが、では具体的にどのようにして実装されているかを
説明致します。



まず、Non Blocking I/Oを理解する上で必要なJava New I/OのAPIを紹介します。

チャネル:

チャネルとは、ハードウェアデバイス、ファイル、ネットワークソケットのほか、
個別の入出力操作を実行できる接続を表します。
Non Blockingにできるチャネルは、java.nio.channels.SelectableChannelクラス
継承したサブクラスで、java.nio.channels.SocketChannel
java.nio.channels.ServerSocketChannelクラスがあります。
このクラスのインスタンスメソッドで、configureBlocking(false)を
実行するとチャネルをNon Blockingモードに調整することができます。

例えば、ServerSocketChannelを使ってチャネルをノンブロッキングモードに
する為には下記のようにserverChannel.configureBlocking(false)を記載します。

selector = Selector.open();
serverChannel = ServerSocketChannel.open();
serverChannel.configureBlocking(false);
serverChannel.socket().bind(new InetSocketAddress(SERVER_PORT));
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
while (selector.select() > 0) {
    for (Iterator it = selector.selectedKeys().iterator(); it.hasNext();) {
        SelectionKey key = (SelectionKey) it.next();
        it.remove();
        if (key.isAcceptable()) {
           doAccept((ServerSocketChannel) key.channel());
        } else if (key.isReadable()) {
            SocketChannel channel = (SocketChannel)key.channel();
            doRead(channel);
       }
    }
}

serverChannelというチャネルをNon Blockingモードに設定した後、
Selectorのインスタンス selector に対してregister(登録)します。
実はこの行がJava New I/Oの中でとても重要な箇所です。

今までのマルチスレッド型のサーバの場合、java.net.ServerSocket.accept()メソッド
を呼び出してソケットのアクセプト処理を行っていましたが、Java New I/Oでは
java.nio.channels.ServerSocketChannel.accept()メソッドでアクセプト処理を行います。
この際、ServerSocketChannel.accept()メソッドはNon Blockingモードに設定されている場合、
保留されている接続がない場合、直ちにnullを返します。
つまりServerSocket.accept()のように新たなクライアントからの接続がくるまで
待ち続けることはありません。

しかし、クライアントからの接続を待たないということはサーバ側ではどのタイミングで
アクセプト処理を行えばよいのかが分からなくなります。
そこで、利用できるチャネル(接続)を取得する為にセレクタ(Selector.select())を使用します。
チャネルをセレクタに登録しておき、セレクタの中で利用できるようになったチャネルを
SelectionKeyとして取得して入出力操作を行うことができます。
セレクタには、複数のチャネルを登録することもでき、
複数の入出力操作を同時に行うことができるようになります。

例えば、下記にかんたんなエコーサーバをJava NIOを使用して
作成してみましたが、下記ではクライアントからの接続毎に
スレッドを生成していないことが御分かりいただけるかと思います。
また、マルチスレッド型のサーバのようにコネクション毎に新規スレッドを
生成していないことがわかります。

例:Java New I/Oで実装したサンプルエコーサーバ
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.util.Iterator;

public class NonBlockingServer {

    private static final int SERVER_PORT = 8888;
    private static final int BUF_SIZE = 2000;

    private Selector selector;

    public static void main(String[] args) {
        NonBlockingServer nserver = new NonBlockingServer();
        nserver.start();
    }


    public void start(){
        ServerSocketChannel serverChannel = null;
        try {
            selector = Selector.open();
            serverChannel = ServerSocketChannel.open();
            serverChannel.configureBlocking(false);
            serverChannel.socket().bind(new InetSocketAddress(SERVER_PORT));
            serverChannel.register(selector, SelectionKey.OP_ACCEPT);
            while (selector.select() > 0) {
                for (Iterator it = selector.selectedKeys().iterator();
                     it.hasNext();) {
                    SelectionKey key = (SelectionKey) it.next();
                    it.remove();
                    if (key.isAcceptable()) {
                        doAccept((ServerSocketChannel) key.channel());
                    } else if (key.isReadable()) {
                        SocketChannel channel = (SocketChannel)key.channel();
                        doRead(channel);
                    }
                }
            }
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    private void doAccept(ServerSocketChannel serverChannel) {
        try {
            SocketChannel channel = serverChannel.accept();
            channel.configureBlocking(false);
            channel.register(selector, SelectionKey.OP_READ);
        } catch (IOException ioe) {
            ioe.printStackTrace();
        }
    }

    private void doRead(SocketChannel channel) {
        ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE);
        Charset charset = Charset.forName("UTF-8");
        try {
            if (channel.read(buf) < 0) {
                return;
            }
            buf.flip();
            System.out.print(
                    charset.decode(buf).toString());
            buf.flip();
            channel.write(buf);
        } catch (IOException ioe) {
           ioe.printStackTrace();
        }
    }
}

このように、Java New I/Oを使用して作成されたサーバは、接続毎に
新規スレッドを作成しなくてもよくなるため、既存のサーバ実装とは
特に大量のアクセス等が発生した際に大きな差がでてきます。
例えば、メモリの消費量も少なくて済むので高負荷時になればなるほど
使用するサーバリソースが大きく変わってくるかと思います。

次回は、Grizzly 1.0.19のソースコードを読む際にどこから見ていけばよいのか、
また、Grizzly 1.0.19の中でどのようなクラスが重要なのか等
ソースコードを見るために必要な情報を紹介したいと思います。

※  Grizzlyのソースコードを読む前に、上記のSelector,SocketChannel
  ServerSocketChannel,SelectionKey等のクラスを理解しておいてください。

Java New IOの参考記事へのURL:

● 横河電機の櫻庭さんによるIT Proの記事「New I/Oで高速な入出力」
● TECHSCORE (5. ノンブロッキングチャネル)

Permalink     No Comments
Track Back :




Post a Comment:
Comments are closed for this entry.