Netty の基本

ここ数日、Nettyについて調べていたので理解できたことを書きます。
JBoss Netty
Netty は Java で非同期、イベント駆動のネットワークアプリを作るためのフレームワークです。Netty を使うと早くて簡単にハイパフォーマンス、ハイスケールでメンテナンス性がいいものが作れます。いいとこ取りの全部乗せです。

  • なぜハイパフォーマンス、ハイスケールか?

Netty は Java NIO(New I/O)をラップしていて、ノンブロッキングなIO操作ができます。そのため、1つのコネクションにずっと1スレッドを割り当てる必要がないため効率のよいリソース消費をします。従来のブロッキングなOIO(Old I/O)もサポートしており、僅かな変更で好きな方を使えます。また、NIOの複雑なByte BufferをChannelBufferというオブジェクトに抽象化し、不必要なコピーが発生しないなどの工夫がされているそうです。

  • なぜメンテナンス性がいいか?

処理をレイヤーごとに分離できるからです。暗号化の層、ビジネスロジックの層などその層だけを気にすればいいように作れます。

Handler と ChannelPipeline

Handler と ChannelPipeline、この2つが Netty の基礎です。これさえ理解すれば大体okです。他の事は大したことではありません。



図の人マークが Handler を表し、そのHandlerをいくつか配置したものが ChannelPipeline です。上方向の流れをUpStreamと言いますが、要は受信です。下方向はDownStreamで、送信に当たります。Serverアプリケーションならリクエストを受け取ってレスポンスを返すと思うので、UpStreamの後にDownStreamが起こります。Clientアプリケーションなら、リクエストを投げてレスポンスを受け取るので、DownStreamの後にUpStreamですね。
流れているものはデータかというと少し違います。イベントです。"データを受信した"、"Openした"、"Closeした"、などのイベントが流れます。データを受信した時のイベントからはデータが取り出せるので、まずはデータが流れていると考えてもいいと思います。
最初にイベントを受け取った最下層のHandler は イベントに入っているデータをdecodeするなどの処理をして一つ上のHandler に処理後のデータをくっ付けたイベントを渡します。このようにHandlerごとに処理を分けることによって、一つ一つのHandlerは独立して自分の仕事に専念できます。独立しているとは言っても、econde/decode処理などを考えると分かるように多くの場合、上下のHandlerに依存性を持ってはいますが。
次のHandlerに処理を渡すとき、すぐに同じスレッドで次のHandlerが処理を始めるわけではありません。各Handlerは上下の順番通りに処理はされるのですが、Handlerごとにスレッドプールから割り当てられたスレッドで処理がされます。
一番上のHandlerまでイベントが渡され、さらに上に渡そうとしたイベントは単に無視されます。Server では多くの場合、一番上のHandlerがリクエストを処理してレスポンスを返す処理を行い、DownStreamが始まります。

先程の図では簡略してPipelineにはUpStreamとDownStreamの2つのラインがあるように書きましたが、正確にはUpStream用のHandlerもDownStream用のHandlerも一つのライン上に配置され、それぞれの方向に対応していないHandlerは単に飛ばされるだけです。

更に言うと、HandlerはUpStreamにもDownStreamにも対応することができるので、こういう図になります。
encode/decode処理、暗号化/解読処理など対の処理は一つのHandlerで処理するのがいいと思われるかもしれませんが、実際にはUpStream部分だけの挙動を変えたいという時があるため、柔軟に変更するためには別々のHandlerにしておいたほうがいいかもしれません。

UpStream に対応するためにはChannelUpstreamHandlerインターフェースを実装する必要があります。

public interface ChannelUpstreamHandler extends ChannelHandler {
    void handleUpstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception;
}

DownStream に対応するためにはChannelDownstreamHandlerインターフェースを実装します。

public interface ChannelDownstreamHandler extends ChannelHandler {
    void handleDownstream(ChannelHandlerContext ctx, ChannelEvent e) throws Exception;
}

それぞれたった一つのメソッドです。Nettyには上記インターフェースを実装した便利なHandlerがいろいろbuilt-inで用意されていて、それを継承してHandlerを作るときは上記のメソッドではなく、他のメソッドをOverrideするだけのことが多いです。しかしそれは handleUpstream/handleDownstream メソッドから他のメソッドが呼び出されているからであり、基本はこのメソッドがUpStream、DownStreamのイベントを処理するということを忘れないでください。引数で渡される ChannelHandlerContext と ChannelEvent とはなんでしょうか。

ChannelHandlerContext

ChannelHandlerContext を使うと ChannelPipeline や他の Handler に作用することができます。

  • イベントの送信

次のHandlerにイベントを渡すためには、必ずこのChannelHandlerContextを使用して明示的にイベントを渡さなければいけません。

ctx.sendUpstream(ChannelEvent)
ctx.sendDownstream(ChannelEvent)

上記メソッドを呼ばないと次のHandlerにイベントが渡されません。
これも built-in の Handler を継承する時はやらないでいいと思う時がありますが、それは継承元のHandlerがやってくれているからです。Netty が自動的に次のHandlerに処理を移してくれよと思うかもしれませんが、framing などのように受信データが一定値まで溜まるまで次のHandlerにイベントを送りたくない時などがあるのでこうなっているんだと思います。

  • ChannelPipelineを動的に変更する
ctx.getPipeline()

を呼ぶとそのHandlerの入っているChannelPipelineが取得でき、Handlerの動的な追加や削除ができます。例えば必要に応じてSSLのHandlerを追加するということができます。<書きかけ。追記予定>

【参考】
jboss netty 3.2 javadoc
javadoc ライクにソースが見れる
チュートリアルとExample

この方のブログ(英語)がとても丁寧に書かれていて参考になりました。是非読んでみてください。
Handler について
NIO FWの比較 パフォーマンス
NIO FWの比較 メモリ
NIO FWの比較 機能
NIO FWの比較 プロトコル