scala nio echo server 改

ScalaといえばErlangの好敵手で、Actorモデルも扱える関数型言語です。
となれば、Actorモデルできれいにnon blocking io を使ったサーバを書いてみたくなるものです。
ということで、scalaのnioを使ったサーバの例を探してみるとyuroyuroさんの書いたすばらしいソースが見つかりました。

http://d.hatena.ne.jp/yuroyoro/20090914/1252903019

しかし、ちょっと読みずらさを感じたので
yuroyuroさんのscalaで書いたnon blocking echo serverを書き換えてみました。

  1. ネストが深いぞー。

私の脳みそはスタックが少ないのでネストがちょっと深いと理解不能に陥ります。
ということで、初期化、セレクト、アクセプト、読み込みをメソッドに分けました。
これで、ネストが深くなくなったぞーっと。

  1. 型がわからんぞー。

知らないメソッドの呼び出し結果の型なんて分かりません。
知っていれば読みやすいのかもしれないけど、自分はnioの初心者なので、何がなんだかわからんぞー。
ということで、型をある程度付け加えて見ました。

  1. メソッドなんだか、変数メンバなんだかわからんぞー

Scalaはすばらしいことにメソッドの引数が0個の場合メソッド呼び出しの括弧を省略できます。
しかし、APIを追っかけるとなると、メソッドなのか、何なのか分からないので逆にしんどかった。
ということで、メソッド呼び出しの括弧はつけました。

  1. packageにしよう。

これは自分の趣味なのですが、packageを書いておくとscalacはフォルダを勝手に掘ってclassファイルを1つのフォルダに入れてくれます。
そうすると、scalaのソースのフォルダとclassのフォルダが別々に分かれて嬉しいので簡単なソースなら1階層のみのパッケージつけると嬉しいです。

  1. セミコロンをつけよう。

今回の場合はJavaに書き換えることも考えていたので、移植の楽さを考えてセミコロンをつけました。
たぶん、Scalaマンセーな状態ならセミコロンは書きません。

  1. close関数は外にもだせるようにしよう。

これも、Javaに移植しなおすときに楽するために。

  1. lazyはいらんだろ。

lazyは遅延評価ってやつで、必要になったら初めて値を計算するものですが、今回の場合別にいらないだろってことで消してみました。

  1. sealedクラスにしよう。

これは逆になくてもいいんだけど、メッセージの仲間たちって感じで明示的にsealedクラスにしたほうがいくね?ってことでしてみました。

  1. StartメッセージをInitメッセージに変える。

startはactorのクラスのメソッド名でもあるので、変えてみました。

  1. メインのobjectは消した。

1個で良いじゃんって思ったので消してみました。

  1. メッセージのクラスはobjectの中に突っ込んでみた。

ほかとは関係ありませんよーってことと、アクターモデルの層をひとかたまりで見れるように近づけて置くといいかなっということで。

  1. まとめ

Java で書くとわずらわしいポーリングして云々の部分は Scala のアクターですっきり。
さらに、アクター部分と処理部の層を分けることでネストが深くなりすぎずに見通しがよくなりました。
処理の中身はjavaのプログラムをscalaシンタックスでよりエレガントに。
それでいて、分かりきった所以外は型をつけることで分かりやすく。
ということで、読みやすくなったんじゃないかなと思いますが、いかがでしょうか?


package echo;

import scala.actors.Actor;
import scala.actors.Actor._;
import scala.collection.jcl.Conversions._;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.{ SelectionKey, Selector, ServerSocketChannel, SocketChannel }
import java.nio.charset.Charset;

object EchoActor extends Actor {
	sealed abstract class Message;
	case class Init(port:Int)           extends Message;
	case class Select()                 extends Message;
	case class Accept(key:SelectionKey) extends Message;
	case class Read(key:SelectionKey)   extends Message;

	def main(args:Array[String]) {
		EchoActor.start();
		EchoActor ! Init(8080);
	}

	def act() {
		loop {
			react {
			case Init(port)  => init(port); EchoActor ! Select;
			case Select      => select();   EchoActor ! Select;
			case Accept(key) => accept(key);
			case Read(key)   => read(key);
			}
		}
	}

	val BUF_SIZE = 1024;
	val selector:Selector = Selector.open();
	val serverChannel:ServerSocketChannel = ServerSocketChannel.open();

	def init(port:Int) {
		serverChannel.configureBlocking(false);
		serverChannel.socket().bind(new InetSocketAddress(port));
		serverChannel.register(selector, SelectionKey.OP_ACCEPT);
		println("EchoServeが起動しました。port=" + port);
	}

	def select() {
		selector.select();
		selector.selectedKeys().foreach { key =>
			if (key.isAcceptable()) {
				EchoActor ! Accept(key);
			} else
			if (key.isReadable()) {
				EchoActor ! Read(key);
			}
		}
	}

	def accept(key:SelectionKey) {
		val socket:ServerSocketChannel = key.channel().asInstanceOf[ServerSocketChannel];
		socket.accept() match {
		case null =>
		case channel:SocketChannel =>
			val remoteAddress:String = channel.socket().getRemoteSocketAddress().toString();
			println(remoteAddress + ":[接続しました]");
			channel.configureBlocking(false);
			channel.register(selector, SelectionKey.OP_READ);
		}
	}

	def read(key:SelectionKey) {
		val channel:SocketChannel = key.channel().asInstanceOf[SocketChannel];
		val remoteAddress = channel.socket().getRemoteSocketAddress().toString();
		val buf:ByteBuffer = ByteBuffer.allocate(BUF_SIZE);

		def close(remoteAddress:String, channel:SocketChannel) {
			channel.close();
			println(remoteAddress + ":[切断しました]");
		}

		channel.read(buf) match {
		case -1 => close(remoteAddress, channel);
		case 0 =>
		case x =>
			buf.flip();
			println(remoteAddress + ":" + Charset.forName("UTF-8").decode(buf).toString());
			buf.flip();
			channel.write(buf);
			close(remoteAddress, channel);
		}
	}

}