読者です 読者をやめる 読者になる 読者になる

Scala で簡易 Web サーバのプロトタイプをリファクタしてみた

Scala

前回Scala で簡易 Web サーバのプロトタイプ書いてみた - etc9の簡易 Web サーバのやっつけ実装を整理してみます。

Handler の依存が気に入りません

Server クラスでは、ServerSocketChannel の初期化を行い、アクセプト要求にこたえる AcceptHandler の登録をしています。その後、selector のセレクト処理を行い、選択された key を登録済みの AcceptHandler で処理するコードとなっています。

class Server(port: Int = 8900) {
  loan(Selector.open) { selector =>
    loan(ServerSocketChannel.open) { channel =>
      channel の初期化処理
      channel.register(selector, SelectionKey.OP_ACCEPT, new AcceptHandler)

        selector のセレクト処理
        while(it.hasNext) {
          ・・・
          key.attachment.asInstanceOf[Handler].handle(key)
        }


登録した AcceptHandler は以下の実装となっています。ServerSocketChannel のアクセプトを行い、ソケット読み込みのためのハンドラである IoHandler を SelectionKey.OP_READ として Selector に登録しています。

class AcceptHandler extends Handler {
  def handle(key: SelectionKey): Unit = if(key.isValid)
    if(key.isAcceptable) {
      val sc: SocketChannel = key.channel.asInstanceOf[ServerSocketChannel].accept
      sc.configureBlocking(false)
      sc.register(key.selector, SelectionKey.OP_READ, new IoHandler)


上記にて、AcceptHandler と IoHandler を直接インスタンス化している部分が気に入りません。new によるインスタンス化は外部で行い、依存性を配線する形に持っていきたいです。

Handler の依存性の配線化

AcceptHandler と IoHandler を trait を積み上げる形で直接的な依存を断ち切っていきます。まずは、AcceptHandler と IoHandler を class から trait に変更します。

trait AcceptHandler extends Handler {
  def acceptHandler: SelectionKey => Unit = ・・・
}
trait IoHandler extends Handler {
  def ioHandler: SelectionKey => Unit = ・・・
}


そうすると 自分型アノテーションによる接続ポイントを加えることで、Server と AcceptHandler にあった目障りな new が無くせます。

class Server(port: Int = 8900) { self: AcceptHandler =>
      ・・・
      channel.register(selector, SelectionKey.OP_ACCEPT, acceptHandler)
trait AcceptHandler { self: IoHandler =>
      ・・・
      sc.register(key.selector, SelectionKey.OP_READ, ioHandler)


合わせて、key.attachment.asInstanceOf は Function1 でキャストするようにしときます。

class Server(port: Int = 8900) { self: AcceptHandler =>
      ・・・
          val key = it.next
          it.remove
          key.attachment.asInstanceOf[SelectionKey => Unit](key)

Channel の入出力を扱う trait が残念なことに

チャネルからの読み込みを行う IoRead と、リクエストの処理を行う Process が、HttpRequestを固定的に扱っており残念な感じです。

trait IoRead {
  private val buffer = IoBuffer(2048)
  def ioRead(key: SelectionKey): Option[HttpRequest] = {
  ・・・
  }
}

trait Process {
  def process(req: HttpRequest)(reply: HttpResponse => Unit) {
  ・・・
  }
}


HttpRequest は型パラメータとして扱うことにして、ついでに入出力と処理は抽象実装にしていきます。
まず、読み込み、読み込んだリクエストの処理、書き込みを表現する trait を抽象実装として定義します。

trait ReadChannel {
  type T
  def read(key: SelectionKey): Option[T]
}

trait Process {
  type T
  type R
  def process(request: T)(reply: R => Unit)
}

trait WriteChannel {
  type R
  def write(key: SelectionKey)
  def reserve(writer: R): Unit
}

後で trait を積み重ねていくので、型パラメータにはしないで、type エイリアスにて型の抽象定義を与えておきました。
これらの trait は IoHandler にミックスインするので、IoHandler に自分型アノテーションを加えて、

trait IoHandler { self: ReadChannel with Process with WriteChannel =>
  def ioHandler: SelectionKey => Unit = key => {
    if (key.isValid && key.isReadable) read(key) map { request =>
      process(request) { response =>
        reserve(response)
        write(key)
      }
    }
    if (key.isValid && key.isWritable) write(key)
  }
}

のような実装になりました。
ReadChannel、Process、WriteChannel の具象は HTTP を扱う別パッケージで実装することにします。

全体としては、

以下のようになり、これを1つのファイルとして Selector と Channnel を扱うモジュールとしときます。クラス名やメソッド名など多少変えています。

class Acceptor(port: Int = 8900) { self: AcceptHandler =>
  withClose(Selector.open) { selector =>
    withClose(ServerSocketChannel.open) { channel =>
      channel.socket.setReuseAddress(true)
      channel.configureBlocking(false)
      channel.socket.bind(new java.net.InetSocketAddress(port))
      channel.register(selector, SelectionKey.OP_ACCEPT, acceptHandler)

      while (selector.keys.size > 0) {
        selector.select
        val it = selector.selectedKeys.iterator
        while(it.hasNext) {
          val key = it.next
          it.remove
          key.attachment.asInstanceOf[Function1[SelectionKey, Unit]](key)
        }
      }
    }
  }
}

trait AcceptHandler { self: IoHandler =>
  def acceptHandler: SelectionKey => Unit = key => if(key.isValid)
    if(key.isAcceptable) {
      val sc: SocketChannel = key.channel.asInstanceOf[ServerSocketChannel].accept
      sc.configureBlocking(false)
      sc.register(key.selector, SelectionKey.OP_READ, ioHandler)
  }
}

trait IoHandler { self: ReadChannel with Process with WriteChannel =>
  def ioHandler: SelectionKey => Unit = key => {
    if (key.isValid && key.isReadable) read(key) map { request =>
      process(request) { response =>
        reserve(response)
        write(key)
      }
    }
    if (key.isValid && key.isWritable) write(key)
  }
}

trait ReadChannel {
  type T
  def read(key: SelectionKey): Option[T]
}

trait Process {
  type T
  type R
  def process(request: T)(reply: R => Unit)
}

trait WriteChannel {
  type R
  def write(key: SelectionKey)
  def reserve(writer: R): Unit
}

ダミー実装をすると、

空の実装ですが、各 trait を積み重ねた IoHandlerProcess を定義すると、

trait IoHandlerProcess extends IoHandler with ReadChannel with WriteChannel with Process {
  type T = String
  type R = String
  def read(key: SelectionKey): Option[String] = None
  def write(key: SelectionKey): Unit = {}
  def reserve(writer: String): Unit = {}
  def process(req: String)(reply: String => Unit): Unit = {}
}


サーバのインスタンス化は以下のようにコンフィグレーションでき、各処理はプラッガブルとなります。

object Main { def main(args: Array[String]) = new Acceptor with AcceptHandler with IoHandlerProcess }

がー、この程度のコードとしては細かく切り過ぎている気がします。せっかくなので公開しますが、もう少し考えてやろう。。