[an error occurred while processing this directive] [an error occurred while processing this directive]

Javaでもselect! (java.nio.channels.Selector)

C/C++でネットワークプログラムというと、selectシステムコールのお世話になることが多いと思います。でもJavaにはselectがありません…でした。でもJava 1.4からはnio(New IO)パッケージの中にSelectorというクラスが用意されています。これでJavaでもSelect!
でもその前にSelectorはnioに含まれていて、これは今までのjava.ioとちょっと流儀が違います。だから、こっちについて先に説明します。

はじめてのnio

とりあえずデータを受け取ってコンソールに表示して終了するプログラム。 起動したサーバーの12345番で待機します。クライアントは書いてないから、telnetで確認してください。
ここで注意しないといけないのは、telnet localhost 12345では受け付けてくれないこと。途中で
 System.err.println("Accepting : " + address.getAddress());
してるけど、ここで表示されるアドレス(ホスト名)に対してtelnetして下さい。
import java.io.IOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;

public class NIOServer {
  ServerSocketChannel ssc;

  public NIOServer(int port) {
    InetSocketAddress address;
    try {
      ssc = ServerSocketChannel.open();
      address = new InetSocketAddress(InetAddress.getLocalHost(), port);
      System.err.println("Accepting : " + address.getAddress());
      ssc.socket().bind(address);
    } catch (IOException e){ e.printStackTrace();}
  }

  public void finalize(){
    try { ssc.close(); } catch(IOException e) { e.printStackTrace(); }
  }
	
  public void start() throws IOException {
    SocketChannel sc = ssc.accept();
    try {
      ByteBuffer buf = ByteBuffer.allocate(1024);
      sc.read(buf);
      buf.flip();
      CharsetDecoder decoder = Charset.forName("ASCII").newDecoder();
      CharBuffer cb = decoder.decode(buf);
      System.out.println("Rcvd : " + cb.toString());
    } catch (IOException e) { e.printStackTrace(); }
    sc.close();
  }
  
  public static void main(String[] args) {
    int port = 12345;
    NIOServer s = new NIOServer(port);
    try{
      s.start();
    } catch(IOException e){
      e.printStackTrace();
    }
  }
}

普通のjava.ioを使った場合に比べて長いですね… でもこれでも最大限シンプルにした結果です。まずアドレスとポートを指定して、サーバーソケットに当たるServerSockerChannelを作ります。そしてこれをバインドして、acceptで待機します。リクエストが来たら、新しいソケットであるSocketChannelが返ってくるので、これを使って読み書きします。読み書きはByteBufferを通して行います。なんとかstreamよりちょっと高速だと期待されるらしい。
(いちおfinalize()書いたのに、Ctrl-cでは呼ばれないっぽい。でも面倒だから放置)

Selector

それでは肝心のSelectorにGo!
まずはSelectorを作ります。new Selector()ではなく、
selector = Selector.open();
で出来ます。
Selectorの実体は、 です。これらのリストはチャネルの状態によって更新されます。初めのリストはkeys()で取得できて、二番目のはselectedKeys()で取得できます。
ではSelectorにチャネル(ソケット)を登録します。普通に考えるとSelectorにchannelを登録するんだからSelectorのメソッドを呼びそうなものなのですが、
  ServerSockerChannel ssc; // (これが登録したいソケット/チャネル)
  ...
  ssc.register(selector, SelectionKey.OP_ACCEPT);
のように、チャネル側のregisterメソッドを呼びます。これでselectorへの登録が完了します。
これでSelectorはチャネルを見張っています。でも、ちゃんと使うにはこっちからメソッドを呼んで状態を調べなければいけません。これはselector.select()で実現されます。これでselectされた数が返されて、エラーの時は-1が返ります。
  while (selector.select() > 0) {
    //(色んな処理)
  }
さてこのループの中では、データが来たりしてセレクトされたチャネルについての操作を書くことになります。このリストは先も書いたとおりselectedKeys()で取得できます。一つ大事なことは、Iteratorで処理するときに、remove()でセレクトされたキーを削除する必要があることです。そうでないと、このチャネルはセレクトされたリストに入り続けてしまいます。
    Iterator itr = selector.selectedKeys().iterator();
    while (itr.hasNext()) {
      SelectionKey key = (SelectionKey)itr.next();
      itr.remove(); //これ大事
      if (key.isAcceptable()) {
        // 新しいコネクションの処理

      } else if (key.isReadable()) {
        // データが到着したから読む
      }
    }
さて、これらをまとめたのが次のミニプログラムです。
import java.io.IOException;
import java.net.*;
import java.nio.*;
import java.nio.channels.*;
import java.nio.charset.*;
import java.util.Iterator;

public class SelectorServer {
  Selector selector;

  public SelectorServer(int port) {
    InetSocketAddress address;
    try {
      selector = Selector.open();
      ServerSocketChannel ssc = ServerSocketChannel.open();
      ssc.configureBlocking(false);
      address = new InetSocketAddress(InetAddress.getLocalHost(), port);
      ssc.socket().bind(address);

      // Register the channel (ssc) to the selector 
      ssc.register(selector, SelectionKey.OP_ACCEPT);
    } catch (IOException e) {	e.printStackTrace();}
  }

//(finalize()は全部書き直し)
  public void finalize(){
    try {
      Iterator itr = selector.keys().iterator();
      while(itr.hasNext()){
        SelectionKey key = (SelectionKey)itr.next();
        Channel c = key.channel();
        System.out.println("Closing " + c.toString());
        c.close();
      }
    } catch(IOException e) { e.printStackTrace(); }
  }

//(start()も全部書き直し)
  public void start() {
    try {
      while (selector.select() > 0) {
        // Get "selected" objects
        Iterator itr = selector.selectedKeys().iterator();
        while (itr.hasNext()) {
          SelectionKey key = (SelectionKey)itr.next();
          itr.remove();
          if (key.isAcceptable()) {
            ServerSocketChannel ssc = (ServerSocketChannel)key.channel();
            SocketChannel sc = ssc.accept();
            sc.configureBlocking(false);
            // register new socket to the selector
            sc.register(selector, SelectionKey.OP_READ);
            System.err.println("Connected : " + sc.socket().toString());
          } else if (key.isReadable()) {
            SocketChannel sc = (SocketChannel) key.channel();
            ByteBuffer buf = ByteBuffer.allocate(1024);
            if (sc.read(buf) < 0) {
              sc.close();
              continue;
            }
            buf.flip();
            CharsetDecoder decoder = Charset.forName("ASCII").newDecoder();
            CharBuffer cb = decoder.decode(buf);
            System.out.println("Rcvd from " + sc.socket().toString());
            System.out.println(" : " + cb.toString());
          }
        }
      }
    } catch (IOException e) {
      e.printStackTrace();
      return;
    }
  }

  public static void main(String[] args) {
    SelectorServer s = new SelectorServer(12345);
    s.start();
  }
}
select()ではビット操作だったのが、Iteratorとかを使ってスマートにまとめられています。でも速さは不明… でも自分で色んなソケットを見張るコードを書くよりは楽かも。
[an error occurred while processing this directive]