CLOVER🍀

That was when it all began.

Netty 3.3.0を触る

このところ、ScalazやGaucheなどちょっと寄り道気味なものを勉強していたので、少し実務が見えるようなものの勉強を始めてみることにしました。

んで、ここはネットワークプログラミングかなぁと思い、以前やっていたJBoss Nettyをもう少し真面目に勉強してみようかと。

今見てみると、JBossから離脱して単体のプロジェクトになっているみたいですね。

Netty
http://netty.io/

Mavenアーティファクトとしても、もはやorg.jbossは付きません。

で、今回はまずはUserGuidから追ってみることにします。
http://netty.io/docs/stable/guide/html/

最初は、DISCARDプロトコルを書くところからだと。「Hello World」じゃないんだぞと。
http://netty.io/docs/stable/guide/html/#start.5
DISCARDというのは、受信したデータを破棄してしまうプロトコルだそうな。

では、やってみましょう。まずは、build.sbtの準備。

name := "netty-discard"

version := "0.0.1"

scalaVersion := "2.9.1"

organization := "littlewings"

libraryDependencies += "io.netty" % "netty" % "3.3.0.Final"

以前はJBossリポジトリを足していましたが、今では不要です。

とりあえず、sbtを起動。

$ sbt
[info] Set current project to netty-discard (in build file:/xxxxx/netty-discard/)

では、まずは例に習いDiscardServerHandlerを写経。
DiscardServerHandler.scala

import org.jboss.netty.channel.{ChannelHandlerContext, ExceptionEvent, MessageEvent, SimpleChannelHandler}

class DiscardServerHandler extends SimpleChannelHandler { // (1)
  override def messageReceived(ctx: ChannelHandlerContext, e: MessageEvent): Unit = { // (2)
    println("receive event")
  }

  override def exceptionCaught(ctx: ChannelHandlerContext, e: ExceptionEvent): Unit = { // (3)
    e.getCause.printStackTrace()

    val ch = e.getChannel
    ch.close()
  }
}

例では、messageReceivedメソッドの中身はホントに空っぽなのですが、デバッグの意味を込めてprintlnを仕込んでいます。

パッケージ名に、「org.jboss」は付いたままなのかと…。

載っている解説を、自分でちゃんと読むことを兼ねてなんとなく訳して書いてみます。


(1)
DiscardServerHandlerクラスは、ChannelHandlerインターフェースの実装の1つであるSimpleChannelHandlerクラスを継承しています。SimpleChannelHandlerの提供する、いくつかのイベントハンドラメソッドをオーバーライドできます。今のところ、ChannelHandlerインターフェースを自分で実装するよりは、SimpleChannelHandlerクラスを継承すれば十分です。
(2)
ここでは、messageReceivedイベントハンドラメソッドをオーバーライドしています。クライアントから新しいデータが送られてきた時は、受信データがMessageEventに設定された状態で呼び出されます。
(3)
exceptionCaughイベントハンドラメソッドは、NettyでI/Oエラーが発生したり、イベント処理中に例外が発生した場合に呼び出されます。ほとんどの場合、ここではロギングと処理対象にしていたChannelをクローズする処理を行うでしょう。とはいえ、例外的な状況では違う実装をすることもあります。例えば、コネクションをクローズする前に、エラーコードを送りたい場合などです。

続いて、DiscardServerです。
DiscardServer.scala

import java.net.InetSocketAddress
import java.util.concurrent.Executors

import org.jboss.netty.bootstrap.ServerBootstrap
import org.jboss.netty.channel.{ChannelFactory, ChannelPipeline, ChannelPipelineFactory, Channels}
import org.jboss.netty.channel.socket.nio.NioServerSocketChannelFactory

object DiscardServer {
  def main(args: Array[String]): Unit = {NioServerSocketChannelFactory
    val factory = new NioServerSocketChannelFactory(Executors.newCachedThreadPool, Executors.newCachedThreadPool) // (4)
    val bootstrap = new ServerBootstrap(factory) // (5)

    bootstrap.setPipelineFactory(new ChannelPipelineFactory { // (6)
      def getPipeline: ChannelPipeline = Channels.pipeline(new DiscardServerHandler)
    })

    bootstrap.setOption("child.tcpNoDelay", true) // (7)
    bootstrap.setOption("child.keepAlive", true)

    bootstrap.bind(new InetSocketAddress(8080)) // (8)
  }
}

(4)
ChannelFactoryインターフェースは、Channelと関連するリソースを作成、管理するファクトリです。全てのI/Oリクエストを処理し、ChannelEventの生成を行います。Nettyは、いくつかのChannelFactoryインターフェースの実装を提供しています。ここでは、サーバサイドアプリケーションの例なので、NioServerSocketChannelFactoryクラスを使用しました。注意点としては、自分自身でI/Oスレッドを生成しないということです。コンストラクタに明示的に与えたスレッド・プールから、スレッドを取得するでしょう。これは、セキュリティ・マネージャを使用するアプリケーションサーバの様に、あなたのアプリケーションが動作する環境でどのようにスレッドを管理するか、より多くの制御方法を与えるでしょう。
(5)
ServerBootstrapクラスは、サーバのセットアップのためのヘルパークラスです。あなたはChannelインターフェースを使用して、直接サーバをセットアップすることができます。しかし、それはつまらない処理ですし、たいていの場合そのようなことをする必要はありません。
(6)
ここでは、ChannelPipelineFactoryの設定をしています。新しいコネクションをサーバが受け入れた時、ここで渡したChannelPipelineFactoryより、ChannelPipelineが生成されます。ここで生成されたパイプラインは、DiscardServerHandlerクラスを保持しています。アプリケーションが複雑になるようなら、あなたは無名クラスをトップレベルのクラスに抽出し、より多くのハンドラクラスをパイプラインに加えるようになるでしょう。
(7)
特定のChannelインターフェースの実装に応じた、パラメータを設定することができます。私たちはTCP/IPサーバを書いているので、tcpNoDelayやkeepAliveのようなソケットのオプションを設定することができます。全てのオプションの前に、「child.」プリフィックスが付与されていることに注意してください。これは、ServerSocketChannelの代わりに、接続されたChannelに適用されることを意味します。ServerSocketChannelにオプションを適用するには、以下のようにします。

bootstrap.setOption("reuseAddress", true)

(8)
準備ができました。ポートにバインドし、サーバを起動します。ここでは、全てのNICの8080ポートにバインドしています。異なるアドレスにバインドするようにすれば、何回もbindメソッドを呼び出すことができます。

おめでとうございます!これでNettyを使った最初のサーバできました!


…って、訳はいろいろ怪しいと思いますが、ガイドが言いたいことはそこそこわかったと思います。(4)が極めて怪しい…。

では、このサーバを起動してみましょう。

> clean
[success] Total time: 0 s, completed Jan 21, 2012 8:13:09 PM
> run
[info] Updating {file:/xxxxx/netty-discard/}default-15c899...
[info] Resolving org.scala-lang#scala-library;2.9.1 ...
[info] Resolving io.netty#netty;3.3.0.Final ...
[info] Done updating.
[info] Compiling 2 Scala sources to /xxxxx/netty-discard/target/scala-2.9.1/classes...
[info] Running DiscardServer 

telnetで8080ポートに繋げて、2回ほどリクエストを送ってみます。

$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
hello
world

sbtコンソールには、イベントを受信した数だけ、以下の様に出力されます。

receive event
receive event

とりあえず、こんな感じで。