Java NIOを用てスケーラブルI/Oを実現する
目次
背景
先日、こちら記事でスケーラブルI/Oの実現方法を調査しました。結果として、1つのスレッドで複数のI/Oを管理することが必要であり、その実現に以下が必要だと分かりました。
- ノンブロッキングI/O
- I/Oの多重化
今回は、上記をJavaで実現する方法を調査します。Java NIOを用いたサンプル実装に関して、後日公開を行う予定です。
ノンブロッキングI/Oの実装方法
ストリームI/OはノンブロッキングI/Oに対応してない為、今回はNIO(New IO)を用いた実装を行います。
NIOとストリームI/Oの比較
従来のストリームI/Oでは、入出力先とのデータパスを、ストリームオブジェクトとして表現しています。そして、それらをつなぎ合わせることで、以下の様にバッファリングやデータ変換機能を追加します。
Socket socket = new Socket("localhost", 8080); InputStream inputStream = socket.getInputStream(); // バッファリング機能の追加 InputStream bufferedInputStream = new BufferedInputStream(inputStream); // プリミティブ型へのデータ変換機能の追加 DataInputStream dataInputStream = new DataInputStream(bufferedInputStream);
一方、NIOでは、以下の通りオブジェクト毎に役割が分担されています。
Channel
: 入出力先とのデータパスを表すオブジェクトです。Buffer
: バッファリングとデータ変換機能を提供するオブジェクトです。
両者の関係は下図の通りです。
Buffer
クラス
Buffer
クラスに関して、仕組みが複雑である為、以下に大まかな仕様をまとめます。
概要
Buffer
は、ある1つの種類のプリミティブ型を格納可能なオブジェクトです。要素の取り込み, 取り出しの2つの用途に使用可能です。
主な属性
capacity
:Buffer
に格納可能な要素数の上限を表します。position
: 要素の取り込み, 取り出しの開始位置を表します。limit
: 要素の取り込み, 取り出しが行えない範囲の先頭の位置を表します。
主な操作
put
メソッド:Buffer
へ要素の取り込みを行うメソッドです。get
メソッド:Buffer
から要素の取り出しを行うメソッドです。flip
メソッド:Buffer
の属性であるposition
とlimit
を、下図の通り"取り込み時の値"と"取り出し時の値"に相互に切り替えを行うメソッドです。compact
メソッド: 下図の通り、以下2つの操作を行うメソッドです。- 既に取り出した要素を捨て、残っている要素を
Buffer
の先頭に移動する。 Buffer
の属性であるposition
とlimit
を、"取り出し時の値"から"取り込み時の値に"に切り替える。
- 既に取り出した要素を捨て、残っている要素を
I/Oの多重化の実装方法
利用するクラス一覧
以下、3つのクラスを利用します。
SelectableChannel
:Selector
に対応したChannel
を表すクラスです。SelectionKey
:SelectableChannel
をSelector
に登録した際の戻り値であり、両者のひも付きを表すクラスです。Selector
: 目的の状態になったChannel
を取得する為のクラスです。
これらのクラスの関係は、下図の通りです。SelectableChannel
を複数のSelector
に登録可能であり、登録毎にSelectionKey
が発行されます。
Selector
を介して、目的の状態を満たしたSelectableChannel
を取得することで、1つのスレッドで複数の接続が管理可能になります。
SelectableChannel
をSelector
に登録する方法
SelectableChannel
クラスのregister
メソッドを用いて、以下の通りSelector
にSelectableChannel
を登録します。
selectableChannel.register(selector, SelectionKey.OP_xxx);
第2引数として、目的の状態を表すのビットパターンの集合を渡します。該当のビットパターンに関して、SelectionKey
において、以下の通り定義されています。
public static final int OP_READ = 1 << 0; // 00001 public static final int OP_WRITE = 1 << 2; // 00100 public static final int OP_CONNECT = 1 << 3; // 01000 public static final int OP_ACCEPT = 1 << 4; // 10000
目的の状態を複数登録する場合は、以下の通り行います。(|
はOR演算子です。)
selectableChannel.register(selector,
SelectionKey.OP_READ | SelectionKey.OP_WRITE); // 101