前回
blog1.mammb.com
の簡易 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 }
がー、この程度のコードとしては細かく切り過ぎている気がします。せっかくなので公開しますが、もう少し考えてやろう。。