javaで、性能を要求されたネットワーク処理を書くときに避けて通れないのが、java.nioパッケージです。特に、このパッケージを使った非ブロックモードのサーバは、多くのリクエストを同時に処理する必要がある場合、必須の機能といえるでしょう。
しかし、どちらかというとUNIXのシステムコールに似たインターフェイスで、少々とっつきにくいパッケージではあります。
その辺を調べる機会があったので、備忘録も兼ねて公開します。
電文フォーマットは、クライアント→サーバ・サーバ→クライアント共に、先頭2バイトがビッグエンディアンのレングスで、その後ろにUTF-8の文字列が続く形式です。
サイズ | データ“UTF-8文字列" | ||||||||||||||
00 | 0F | 55 | 54 | 46 | 2D | 38 | E6 | 96 | 87 | E5 | AD | 97 | E5 | 88 | 97 |
クライアントからリクエストを受信すると、サーバはレスポンスを返し、サーバからのレスポンスを受信したクライアントは、サーバに処理完了を投げて、クライアント側は処理終了となります。サーバはクライアントからの処理完了を受信して終了となります。
最初に、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#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クラスも、サーバとほぼ同じです。処理の順序が逆になるので、最初の操作としてSelectionKey.OP_WRITEを返します。
書込みが終了すると、SelectionKey.OP_READを返し、読み込みの後SelectionKey.OP_WRITEを返します。2度めの書込みの後、falseを返して処理終了です。
クライアント側にjava.nioの処理が必要がどうかはわかりませんが、プロトコルの制御処理を切り分けて、サーバとクライアントで対の形で実装できるのは、便利かもしれません。
特別変わった箇所はありません。
SocketChannel#connectメソッドの呼び出しで、サーバとの通信が始まります。
本来は、このメソッドの戻り値を判断して、SocketChannel#finishConnect呼び出しの要否を判断するのですが、非ブロックモードでは必須なので、判断していません。
この例では、クライアント側からクローズしていますが、サーバ側もクローズしないと正しく接続が切れないので注意が必要です。