Javaでネットワーク、nio編

java.nio

javaで、性能を要求されたネットワーク処理を書くときに避けて通れないのが、java.nioパッケージです。特に、このパッケージを使った非ブロックモードのサーバは、多くのリクエストを同時に処理する必要がある場合、必須の機能といえるでしょう。

しかし、どちらかというとUNIXのシステムコールに似たインターフェイスで、少々とっつきにくいパッケージではあります。

その辺を調べる機会があったので、備忘録も兼ねて公開します。

まずは簡単な、前提となるプロトコルの説明

電文フォーマットは、クライアント→サーバ・サーバ→クライアント共に、先頭2バイトがビッグエンディアンのレングスで、その後ろにUTF-8の文字列が続く形式です。

電文のフォーマット
サイズデータ“UTF-8文字列"
000F5554462D38E69687E5AD97E58897

クライアントからリクエストを受信すると、サーバはレスポンスを返し、サーバからのレスポンスを受信したクライアントは、サーバに処理完了を投げて、クライアント側は処理終了となります。サーバはクライアントからの処理完了を受信して終了となります。

java.nioを使用した、ネットワーク上のシーケンス

サーバ側制御処理のコードサンプル

最初に、java.nio.channelsのSelectorを使ったネットワーク処理の基本となるコードです。

ここでは、業務的な処理は何もしていません。業務的な処理は、次に紹介するProtocolクラスが担います。

このコードに出てくる内容は、java.nioを使ったネットワークの解説に出てくるそのままだと思いますので、個別に実装した部分だけ簡単に解説します。

クライアントから接続された時点で、Protocolクラスのインスタンスを作成します(31行目)。

最初にクライアントからの入力を読み込むか、クライアントに出力するかは、Protocolクラスの指示に準ずるので、32行目でProtocol#getOpメソッドで次の操作を取得し、SocketChannel登録の引数として渡します。また、Protocolクラスのインスタンスを第3引数に渡して、SocketChannelに紐付いたオブジェクトとして登録します。

読み込み操作の40行目でProtocolクラスのインスタンスを取得します。43行目でProtocol#readメソッドでデータを読み込みます。戻り値がtrueの場合は継続ありなので、44行目で次の操作を取得して、設定します。

readメソッドがfalseを返した場合は、処理終了なのでチャネルをクローズします(46行目)。

書込み操作も、読み込み操作とほぼ同じです。

サーバ側Protocolクラスのコードサンプル

業務処理を制御するProtocolクラスです。

Protocol#writeメソッドは、データを編集して書き込むだけなので、Protocol#readメソッドを簡単に解説します。

やっていることは簡単で、初期状態では2バイト分バッファをアロケートし(14行目)、18行目でデータを読み込みます。

2バイト読み込めたら、20行目以降でデータのサイズを取得し、dataLengthに格納します。

dataLengthが取得できたら、16行目でそのサイズ分バッファをアロケートし、18行目で読み込みます。

必要なサイズを読み込んだら、27行目以降で文字列に変換し、ログに出力します(実際に業務で使用する場合は、ここでキューに登録するとかします)。

31行目の判定は、1回目の読み込みではfalseなので、38行目以降を実行します。この時点で次の操作はSelectionKey.OP_WRITEとなります。

書込みがwriteメソッドで実行されると、53行目でisWriteがtrueとなるので、2回めの読み込みが完了すると、32行目に制御が渡り、falseを返すので処理が終了します。

但し、クライアントに何かを返すまでに時間がかかる場合は、多少の考慮が必要となります。

今時のネットワークは全二重通信が普通なので、書込みはいつでもできる状態です(ちなみに、読み込みは相手が書き込んだデータが残っている間だけ可能です)。

次の操作をSelectionKey.OP_WRITEとして登録すると、書き込むデータが用意できるまでの間、「java.nioによるサーバ」ソースの14行目がブロック無しで返るので、wait無しのループ状態(無限ではありませんが)となり、CPUをガッツリ使うこととなります。

このようなケースでは、最初の読み込みが完了した時点で別スレッドを上げて、その中で書き込むデータが用意できるのを待ち、書き込みを行うことで、操作をSelectionKey.OP_WRITEとはしないといった処理が必要でしょう。

今回の例ではProtocolクラスのインスタンスをnew演算子を使って作成していますが、ファクトリクラスを用意してIPアドレスをキーにすれば、端末毎に異なるプロトコルで通信することも可能です(同じポートでそれほど複雑な処理が必要かどうかは分かりませんが)。

クライアント側制御処理のコードサンプル

基本的な作りは、サーバ側と同じです。

Selectorで状態変化を監視する対象は、ServerSocketChannelではなくSocketChannelとなります。

最初の操作指示は、10行目の通りSelectionKey.OP_ACCEPTではなくSelectionKey.OP_CONNECTです。ループ内の最初の条件判定もkey.isConnectable()です。

ソケットチャネルをクローズした後、44行目と58行目でSelector#wakeupを呼び出します。これにより、直後のSelector#select(15行目)が処理をブロックせず0を返すので、ループを抜けて終了します。

クライアント側Protocolクラスのコードサンプル

Protocolクラスも、サーバとほぼ同じです。処理の順序が逆になるので、最初の操作としてSelectionKey.OP_WRITEを返します。

書込みが終了すると、SelectionKey.OP_READを返し、読み込みの後SelectionKey.OP_WRITEを返します。2度めの書込みの後、falseを返して処理終了です。

クライアント側にjava.nioの処理が必要がどうかはわかりませんが、プロトコルの制御処理を切り分けて、サーバとクライアントで対の形で実装できるのは、便利かもしれません。

ネットワーク上のシーケンス

特別変わった箇所はありません。

SocketChannel#connectメソッドの呼び出しで、サーバとの通信が始まります。

本来は、このメソッドの戻り値を判断して、SocketChannel#finishConnect呼び出しの要否を判断するのですが、非ブロックモードでは必須なので、判断していません。

この例では、クライアント側からクローズしていますが、サーバ側もクローズしないと正しく接続が切れないので注意が必要です。

java.nioを使用した、ネットワーク上のシーケンス

サンプルソース

サーバ側サンプルの全ソース

クライアント側サンプルの全ソース