CLOVER🍀

That was when it all began.

ClojureとScalaで、Echo Serverを書く(復習編)

少し、ネットワークプログラミングの勉強をしようと思いまして。

ホントはUDPを中心にやりたいのですが、まずは以前TCPで復習をしてみようかなということで。1年ちょっと前に、こんなエントリを書きました。

Clojure(とScalaとGroovy)でEcho Serverを書く
http://d.hatena.ne.jp/Kazuhira/20120520/1337512349

Clojureの入出力系の関数を覚えたところでお題としてやってみました、みたいな感じでしたが、今回は単純に復習ですね。それと、Groovyは外したいと思います。

スタンスとしては、Clojureを学びつつネットワークプログラミングの基礎ができたら、というところでしょうか。ただ、Clojureだけだと不安なのでScalaでのリライトも入れていきます。

今回のEcho Serverの仕様は、以下の通りです。基本的には、前と同じ。

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

で、1年ちょっと前から今になって書き直したコードはこちら。まずはClojure版。

echo_server.clj

(import '(java.net InetSocketAddress ServerSocket)
        '(java.util Date))
(require '[clojure.java.io :as io]
         '[clojure.string :as string])

(def address
  (case (count *command-line-args*)
    0 (InetSocketAddress. 8080)
    1 (InetSocketAddress. (read-string (first *command-line-args*)))
    2 (InetSocketAddress. (first *command-line-args*)
                          (read-string (second *command-line-args*)))
    (throw (IllegalArgumentException. "Require 0-2 Arguments!!"))))

(defn log [word & more-words]
  (println (str \[ (Date.) \] \  (string/join \  (conj more-words word)))))

(defn accepted-client [socket]
  (with-open [s socket
              r (io/reader (.getInputStream s) :encoding "UTF-8")
              w (io/writer (.getOutputStream s) :encoding "UTF-8")]
    (.write w "Welcome to Simple Clojure Echo Server!!")
    (.write w "\r\n")
    (.write w "\r\n")
    (.flush w)

    (doseq [word (take-while #(and (not (nil? %)) (not (= % "exit")))
                             (repeatedly #(.readLine r)))]
      (when-not (empty? word)
        (.write w (str "You Say => " word "\r\n"))
        (.flush w)))

    (when (not (.isClosed socket))
      (.write w "ByeBye!!\r\n")
      (.flush w)
      (log "Disconnect Client" "=>" s))))

(with-open [server-socket (ServerSocket.)]
  (.bind server-socket address)
  (log "Simple Clojure Echo Server" (str \[ address \]) "Startup.")

  (doseq [socket (repeatedly #(.accept server-socket))]
    (log "Accept New Client" "=>" socket)
    (.start (Thread. #(accepted-client socket)))))

前と変わったのは、loop/recurで書いたいたところを、repeatedlyにして再帰を明示しないようにしたところでしょうかね。あと、もうちょっとまともにwith-openを使うようになったところ?

代わりといってはなんですが、reader/writer関数の存在を忘れていたり、condpも記憶からなくなってたりでいろいろ苦労…いったん書いた後、Scala版を書いた後に見直してこういう形になりました。

前よりはスッキリしたんじゃないかな?

そして、Scala版はこちらです
EchoServer.scala

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

import EchoServer.AutoCloseableWrapper

object EchoServer {
  def main(args: Array[String]): Unit = {
    val address = args.toList match {
      case Nil => new InetSocketAddress(8080)
      case port :: Nil => new InetSocketAddress(port.toInt) 
      case host :: port :: Nil => new InetSocketAddress(host, port.toInt)
      case _ => throw new IllegalArgumentException("Require 0-2 Arguments!!")
    }

    new EchoServer(address).start()
  }

  implicit class AutoCloseableWrapper[A <: AutoCloseable](val underlying: A) extends AnyVal {
    def foreach(fun: A => Unit): Unit =
      try {
        fun(underlying)
      } finally {
        underlying.close()
      }
  }
}

class EchoServer(address: InetSocketAddress) {
  def start(): Unit =
    for (serverSocket <- new ServerSocket) {
      serverSocket.bind(address)
      log("Simple Scala Echo Server", address, "Startup.")

      Iterator
        .continually(serverSocket.accept())
        .foreach { s =>
          log("Accepted New Client", "=>", s)
          new Thread {
            override def run(): Unit = acceptedClient(s)
          }.start()
        }
  }

  private def acceptedClient(clientSocket: Socket): Unit =
    for {
      socket <- clientSocket
      reader <- new BufferedReader(new InputStreamReader(socket.getInputStream, StandardCharsets.UTF_8))
      writer <- new BufferedWriter(new OutputStreamWriter(socket.getOutputStream, StandardCharsets.UTF_8))
    } {
      writer.write("Welcome to Simple Scala Echo Server!!")
      writer.write("\r\n")
      writer.write("\r\n")
      writer.flush()

      Iterator
        .continually(reader.readLine())
        .takeWhile(word => word != null && word != "exit")
        .withFilter(!_.isEmpty)
        .foreach { word =>
          writer.write(s"You Say => $word")
          writer.write("\r\n")
          writer.flush()
        }

      if (!socket.isClosed) {
        writer.write("ByeBye!!")
        writer.write("\r\n")
        writer.flush()

        log("Disconnect Client", "=>", socket)
      }
    }

  private def log(messages: Any*): Unit =
    println(s"[${new Date}] ${messages.mkString(" ")}")
}

こっちは、なんかすごいさらっと書けました。前と違って、自分で末尾再帰を書いたり内部関数を使ったりしてないので、なおスッキリ。リソースのクローズは、for式で書けるようにしました。

まあ、行数自体はあんまり縮んでいないのですが、書きっぷりとしては前進したんじゃないかなぁと。

…とにかく、Clojure力の無さを思い知ったお題でした。