少し、ネットワークプログラミングの勉強をしようと思いまして。
ホントは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力の無さを思い知ったお題でした。