CLOVER🍀

That was when it all began.

Clojure(とScalaとGroovy)でEcho Serverを書く

Clojureの入出力系の関数を一通り目を通した(と思っている)ので、ちゃんとアプリっぽいものを書いてみようとClojureでEcho Serverを書くことにチャレンジしてみました。

今回のEcho Serverの動作は、以下の通りです。

  • 起動引数にポート番号、またはホスト名とポート番号のペアを取る
  • 入力された文字に「You Say => 」をくっつけて、クライアントに送信する
  • 「exit」と入力された場合は、「ByeBye!」と送信して接続を切る
  • 空文字(単純にEnterを押されただけ)を入力された場合は、無視して次の入力を待つ
  • クライアントの接続ごとに、マルチスレッドで動作する

先に結果から。できたコードはこちら。
echo_server.clj

(import '(java.net InetSocketAddress ServerSocket)
        '(java.util Date))
(use '[clojure.java.io :only (reader writer)])

(defn usage
  []
  (println "Required 1 or 2 arguments")
  (println "\tone argument => port")
  (println "\ttwo arguments => ip or hostname, port")
  (System/exit 1))

(defn log
  [word & more-words]
  (println (str "[" (Date.) "] " (apply str word more-words))))

(def server-socket
  (let [s (ServerSocket.)]
      (condp = (count *command-line-args*)
        1 (do (.bind s
                     (InetSocketAddress.
                      (read-string (first *command-line-args*)))) s)
        2 (do (.bind s
                     (InetSocketAddress.
                      (first *command-line-args*)
                      (read-string (second *command-line-args*)))) s)
        (usage))))

(log "Simple Clojure Echo Server Startup")

(defn echo-talk
  [socket]
  (with-open [s socket
              r (reader (.getInputStream s) :encoding "UTF-8")
              w (writer (.getOutputStream s) :encoding "UTF-8")]
    (letfn [(bye []
              (do (.write w (str "ByeBye!" "\r\n"))
                  (.flush w)
                  (log "Disconnect Client => " (.toString socket)) nil))]
      (loop [word (.readLine r)]
        (condp = word
          nil nil
          "exit" (bye)
          "" (recur (.readLine r))
          (do (.write w (str "You Say => " word "\r\n"))
            (.flush w)
            (recur (.readLine r))))))))

(loop [client-socket (.accept server-socket)]
  (log "Accept New Client => " (.toString client-socket))
  (.start (Thread. #(echo-talk client-socket)))
  (recur (.accept server-socket)))

割と苦労したのは、この関数

(defn echo-talk
  [socket]
  (with-open [s socket
              r (reader (.getInputStream s) :encoding "UTF-8")
              w (writer (.getOutputStream s) :encoding "UTF-8")]
    (letfn [(bye []
              (do (.write w (str "ByeBye!" "\r\n"))
                  (.flush w)
                  (log "Disconnect Client => " (.toString socket)) nil))]
      (loop [word (.readLine r)]
        (condp = word
          nil nil
          "exit" (bye)
          "" (recur (.readLine r))
          (do (.write w (str "You Say => " word "\r\n"))
            (.flush w)
            (recur (.readLine r))))))))

で、最初はうまいことloop/recurの組み合わせが動かせずにてこずりました。do特殊形式を使い過ぎかなぁ?もうちょっと精進が必要な気がします。
なお、with-openは最後に与えられた引数からcloseしていくことを、今回学びました。

入出力系以外では、condpとread-stringが初めて使った関数ですね。

動かし方は、こんな感じ。

$ clj echo_server.clj 8080
[Sun May 20 20:05:20 JST 2012] Simple Clojure Echo Server Startup

あとは、適当なクライアントからtelnetなどでつなげます。

$ telnet localhost 8080
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
Hello
You Say => Hello
World
You Say => World
exit
ByeBye!
Connection closed by foreign host.

サーバ側のコンソールには、こんなログが出ます。

[Sun May 20 20:05:59 JST 2012] Accept New Client => Socket[addr=/127.0.0.1,port=55616,localport=8080]
[Sun May 20 20:06:04 JST 2012] Disconnect Client => Socket[addr=/127.0.0.1,port=55616,localport=8080]

一応、マルチスレッドで動作するので複数クライアントで接続できますが、こちらは割愛。

では、ScalaとGroovyによるリライトシリーズ。

まずは、Scala
EchoServer.scala

import Resources._

import scala.annotation.tailrec

import java.io.{BufferedReader, BufferedWriter, InputStreamReader, OutputStreamWriter}
import java.net.{InetSocketAddress, ServerSocket, Socket}
import java.util.Date

object EchoServer {
  def main(args: Array[String]): Unit = {
    val echoServer = args toList match {
      case port :: Nil =>
        new EchoServer(new InetSocketAddress(port.toInt))
      case hostname :: port :: Nil =>
        new EchoServer(new InetSocketAddress(hostname, port.toInt))
      case _ => usage()
    }

    echoServer.start()
  }

  def usage(): Nothing = {
    println("""|Required 1 or 2 arguments
               |  one argument => port
               |  two arguments => ip or hostname, port""".stripMargin)
    sys.exit(1)
  }

  def log(words: Any*): Unit = println("[" + new Date + "] " + words.mkString(""))
}

class EchoServer(address: InetSocketAddress) {
  def start(): Unit = using(new ServerSocket) { serverSocket =>
    serverSocket.bind(address)
    EchoServer.log("Simple Scala Echo Server Startup")
    acceptWaiting(serverSocket)
  }

  @tailrec
  private def acceptWaiting(serverSocket: ServerSocket): Unit = {
    val socket = serverSocket.accept()
    EchoServer.log("Accept New Client => ", socket)
    val thread = new Thread { override def run: Unit = echoTalk(socket) }
    thread.start()
    acceptWaiting(serverSocket)
  }

  private def echoTalk(socket: Socket): Unit = using(socket) { s =>
    using(new BufferedReader(new InputStreamReader(s.getInputStream, "UTF-8"))) { reader =>
      using(new BufferedWriter(new OutputStreamWriter(s.getOutputStream, "UTF-8"))) { writer =>
        def bye(): Unit = {
          writer.write("ByeBye!\r\n")
          writer.flush()
          EchoServer.log("Disconnect Client => ", s)
        }

        @tailrec
        def echoTalkInner(): Unit = reader.readLine() match {
          case null =>
          case "exit" => bye()
          case "" => echoTalkInner()
          case word =>
            writer.write("You Say => " + word + "\r\n")
            writer.flush()
            echoTalkInner()
        }
        echoTalkInner()
      }
    }
  }
}

object Resources {
  type Closeable = { def close(): Unit }

  def using[A <: Closeable, B](resource: A)(body: A => B): B =
    try {
      body(resource)
    } finally {
      resource.close()
    }
}

Clojureと違って、with-openみたいなものが標準にないので、ムダに長い…。あと、どうしてもwhile文を避けたくなる(笑)のでローカル関数が多めに見えますね。

動作はClojure版と同じですが起動時に

$ scala EchoServer 8080
[Sun May 20 20:09:20 JST 2012] Simple Scala Echo Server Startup

と若干メッセージが変わります。

次いで、Groovy。Groovy JDKがあるため、こちらはかなり楽。
echo_server.groovy

def serverSocket

switch (args.size()) {
    case 1:
        serverSocket = new ServerSocket()
        serverSocket.bind(new InetSocketAddress(args[0].toInteger()))
        break
    case 2:
        serverSocket = new ServerSocket()
        serverSocket.bind(new InetSocketAddress(args[0], args[1].toInteger()))
        break
    default:
        usage()
        break
}

log("Simple Groovy Echo Server Startup")
while (true) {
    serverSocket.accept(this.&echoTalk)
}

def echoTalk(socket) {
    log("Accept New Client => ${socket}")
    def bye = { writer ->
        writer.write("ByeBye!\r\n")
        writer.flush()
        log("Disconnect Client => ${socket}")
    }

    socket.inputStream.withReader("UTF-8", { reader ->
        socket.outputStream.withWriter("UTF-8", { writer -> 
            def line
            while ((line = reader.readLine()) != null) {
                if (line == "") {
                    continue
                } else if (line == "exit") {
                    bye(writer)
                    break
                } else {
                    writer.write("You Say => ${line}\r\n")
                    writer.flush()
                }
            }
        })
    })
}

def log(Object... words) {
    println("[${new Date()}] " + words.join(""))
}

def usage() {
    println("""|Required 1 or 2 arguments
               |\tone argument => port
               |\ttwo argument => ip or hostname, port""".stripMargin())
    System.exit(1)
}

ServerSocket#acceptが引数に取るClosureはデフォルトで別スレッドで動作するので、Threadを直接使用するコードは登場しません。with〜系のメソッドがあるので、closeの呼び出しが登場しないところも楽ですね。

でも、もうちょっと楽な書き方があるんじゃないかなぁ〜とも思いますが…。

ところで、String#stripMarginなんてあるんですね。

こちらも、起動時は若干自己主張します。

$ groovy echo_server.groovy 8080
[Sun May 20 20:12:15 JST 2012] Simple Groovy Echo Server Startup