5. ノンブロッキングチャネル
- 5.1. SocketChannel/ServerSocketChannel
- 5.2. ノンブロッキング入出力
5.1. SocketChannel/ServerSocketChannel
従来の java.net パッケージを使用した入出力では、ソケットの accept メソッドや read メソッドなどを呼び出すと接続や入力があるまで処理が待ち状態になりました。 このような入出力待ちの動作のことをブロックといいます。
接続の待ちうけでブロックが発生するため、複数のネットワーク接続を同時に処理するサーバアプリケーションを実装するにはマルチスレッドを利用する必要がありました。 マルチスレッドを利用したサーバアプリケーションの作成方法はネットワークプログラミング3章で説明しています。
しかしスレッドの生成はそれなりにコストのかかる処理であり、アクセスの多いサーバではその影響が無視できないくらい大きくなります。 そこで NIO ではブロックの発生しない入出力を実現する方法が提供されました。 ブロックされない入出力を利用すると、1つのスレッドでも複数の入出力を見かけ上同時に処理することができるようになります。
NIO でネットワーク入出力を行うためには SocketChannel や ServerSocketChannel を利用します。これらはそれぞれ、java.io パッケージの Socket や ServerSocket に相当します。これらのクラスはブロックする入出力とブロックしない入出力のどちらも利用することができます。
まず、Socket や ServetSocket と同じようにブロックする操作の利用方法を説明します。例として、文字列の送受信を行うサーバ・クライアントアプリケーションを示します。最初はサーバ側のアプリケーションです。
ChannelEchoServer.java
package nio.chapter5; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; public class ChannelEchoServer { public static final int ECHO_PORT = 10007; public static void main(String[] args) { new ChannelEchoServer().run(); } public void run() { ServerSocketChannel serverChannel = null; try { serverChannel = ServerSocketChannel.open(); serverChannel.socket().bind(new InetSocketAddress(ECHO_PORT)); System.out.println("ChannelEchoServerが起動しました(port=" + serverChannel.socket().getLocalPort() + ")"); while (true) { SocketChannel channel = serverChannel.accept(); System.out.println(channel.socket().getRemoteSocketAddress() + ":[接続されました]"); new Thread(new ChannelEchoThread(channel)).start(); } } catch (IOException e) { e.printStackTrace(); } finally { if (serverChannel != null && serverChannel.isOpen()) { try { System.out.println("ChannelEchoServerを停止します。"); serverChannel.close(); } catch (IOException e) {} } } } } class ChannelEchoThread implements Runnable { private static final int BUF_SIZE = 1000; SocketChannel channel = null; public ChannelEchoThread(SocketChannel channel) { this.channel = channel; } public void run() { ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE); Charset charset = Charset.forName("UTF-8"); String remoteAddress = channel.socket() .getRemoteSocketAddress() .toString(); try { if (channel.read(buf) < 0) { return; } buf.flip(); String input = charset.decode(buf).toString(); System.out.print(remoteAddress + ":" + input); buf.flip(); channel.write(buf); } catch (IOException e) { e.printStackTrace(); return; } finally { System.out.println(remoteAddress + ":[切断しました]"); if (channel != null && channel.isOpen()) { try { channel.close(); } catch (IOException e) {} } } } }ChannelEchoServer クラスが直接起動されるクラスです。
serverChannel = ServerSocketChannel.open(); serverChannel.socket().bind(new InetSocketAddress(ECHO_PORT));
ServerSocketChannel のコンストラクタは public ではなく、インスタンスを生成するには static な open() メソッドを呼び出します。この時点ではまだポートにバインドされていないため、利用するにはバインドする必要があります。しかし、ServerSocketChannel クラスにはバインドを実現するメソッドがありません。ServerSocketChannel は内部的に Socket を利用しており、その Socket オブジェクトの bind メソッドを利用してバインドを行います。Socket オブジェクトは ServerSocketChannel の socket メソッドで取得することができます。
while (true) { SocketChannel channel = serverChannel.accept(); System.out.println(channel.socket().getRemoteSocketAddress() + ":[接続されました]"); new Thread(new ChannelEchoThread(channel)).start(); }
接続の待ち受けは ServerSocket と同じように accect() メソッドを利用します。ここでは接続があるまで処理がブロックされます。accept() メソッドの戻り値は SocketChannel であり、この Channel を利用してデータの入出力を行います。
if (channel.read(buf) < 0) { return; } buf.flip(); String input = charset.decode(buf).toString(); System.out.print(remoteAddress + ":" + input); buf.flip(); channel.write(buf);
このプログラムでは、接続ごとに新しいスレッドを作成して複数の同時接続を処理できるようにしています。ChannelEchoThread クラスで別スレッドで起動する処理を記述しています。 このあたりのプログラムの書き方は java.net の Socket を利用した場合とほとんど同じです。SocketChannel はその名前の通り Channel を実装していますので、Channel のインタフェースを利用して入出力を行うことができます。ここでの read() メソッドはデータが受信されるまで処理をブロックします。
次にクライアント側のプログラムを示します。ChannelEchoClient.java
package nio.chapter5; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.nio.channels.SocketChannel; import java.nio.charset.Charset; public class ChannelEchoClient { public static final int ECHO_PORT = 10007; public static final int BUF_SIZE = 1000; public static void main(String[] args) { SocketChannel channel = null; ByteBuffer buf = ByteBuffer.allocate(BUF_SIZE); Charset charset = Charset.forName("UTF-8"); try { channel = SocketChannel.open(new InetSocketAddress("localhost", ECHO_PORT)); BufferedReader keyin = new BufferedReader(new InputStreamReader(System.in)); System.out.print("送信:"); String line = keyin.readLine(); channel.write(charset.encode(CharBuffer.wrap(line + "\n"))); while (channel.isConnected()) { buf.clear(); if (channel.read(buf) < 0) { return; } buf.flip(); System.out.print("受信:" + charset.decode(buf).toString()); } } catch (IOException e) { e.printStackTrace(); } finally { if (channel != null && channel.isOpen()) { try { channel.close(); } catch (IOException e) {} } } } }
SocketChannel は ServerSocketChannel と同じように static の open() メソッドでインスタンスを取得します。
channel = SocketChannel.open(new InetSocketAddress("localhost", ECHO_PORT));
SocketAddress を引数に取る open() メソッドでは、インスタンスの取得に加えて接続処理まで一度に行われます。ここでは同じマシン上で動作しているサーバアプリケーションに対して接続を行っています。
(実習課題1)
例で示したクライアントプログラムはローカルのマシンで動作するサーバにしか接続できないプログラムであるが、これをコマンドライン引数で指定した任意のホスト・ポート上で待ち受けるサーバに接続できるように改良しなさい。