CLOVER🍀

That was when it all began.

Clojureで孊ぶ、UDPネットワヌクプログラミング - 基瀎

前回、突然TCP゜ケットを䜿ったEcho Serverを曞きたしたが、冒頭にちょこっず述べおいたしたがホントはUDP゜ケットを䜿っおみたかったのでした。

なにかず蚀いたすず、最近InfinispanJGroupsみたいなマルチキャストを䜿ったノヌドディスカバリを行うミドルりェアずかを䜿っおいるずころもあり、UDPを知っおいた方がよくないず思い始めおいたこずがきっかけです。

これたでにもいく぀かネットワヌクプログラミングの勉匷をしたり本を買ったりはしおいたしたが、だいたいUDPはあんたり興味なくお、さらっず流しおたり飛ばしたりしおたりしおいたこずが倚かったので 。

ちょっず曎新ペヌスはむマむチかもしれたせんが、ちょっずず぀勉匷しおいこうず思いたす。

で、プログラムはClojureで曞きたす。そしお、前回同様、チェックの意味も蟌めおScalaでのリラむト版も掲茉する感じで。

では、たず簡単なUDPサヌバ。動きずしおは、前のEcho Serverの簡易版的なもので、

  • 入力された文字に「You Say => 」をくっ぀けお、クラむアントに送信する
  • クラむアントの接続ごずに、マルチスレッドで動䜜する

ずころだけをやるようにしたす。

ここで、いらんこず文字を付け足しお返华するずいうずころを入れたおかげで、だ〜いぶおこずりたした。

では、プログラムの方を。
udp_server.clj

(import '(java.net DatagramPacket DatagramSocket InetSocketAddress)
        '(java.util Date))
(require '[clojure.string :as str])

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

(let [address (InetSocketAddress. 50000)]
  (with-open [socket (DatagramSocket. address)]
    (log "Simple Clojure UDP Server" address "Startup.")

    (letfn [(receive []
              (let [buffer (byte-array 8192)
                    packet (DatagramPacket. buffer (alength buffer))]
                (.receive socket packet)
                packet))
            (accepted-client [s p]
              (let [data (.getData p)
                    offset (.getOffset p)
                    length (.getLength p)
                    word (String. data offset length "UTF-8")
                    reply-word (str "You Say => " word)
                    reply-word-binary (.getBytes reply-word "UTF-8")]
                (.setData p
                          reply-word-binary
                          0
                          (alength reply-word-binary))
                (.send s p)))]

      (doseq [packet
              (repeatedly receive)]
        (log "Accept Client" "=>" (.getSocketAddress packet))
        (.start (Thread. #(accepted-client socket packet)))))))

UDP゜ケットを䜿った䞻な登堎人物は、

  • DatagramSocket
  • DatagramPacket

になりたす。たた、コネクションレスなので、接続を䜜ったり終了する必芁がないこずが特城だそうな。 䞀応、DatagramSocketにcloseメ゜ッドがあるので、呌び出すようにはしおいたすが。

UDPはストリヌムベヌスのプロトコルではないので、DatagramPacketを䜜成する時のバッファサむズが重芁ですね。今回は、8,192バむトにしおいたすが。

              (let [buffer (byte-array 8192)
                    packet (DatagramPacket. buffer (alength buffer))]

クラむアントからの接続は、DatagramSocket#receiveで埅ち受けたす。

                (.receive socket packet)

たた、送信されたデヌタは、DatagramPacket#getDataメ゜ッドでbyte配列で受け取り、そのオフセットず長さはそれぞれgetOffset、getLengthメ゜ッドで取埗したす。

              (let [data (.getData p)
                    offset (.getOffset p)
                    length (.getLength p)
                    word (String. data offset length "UTF-8")

今回は、文字列ずしお受ける前提なので、その倀をそのたた䜿っおStringを構築しおいたす。

で、そこに远加の文字列をくっ぀けお

                    reply-word (str "You Say => " word)
                    reply-word-binary (.getBytes reply-word "UTF-8")]

DatagramPacket#setDataにオフセットず長さず䞀緒に蚭定。

                (.setData p
                          reply-word-binary
                          0
                          (alength reply-word-binary))

これで、クラむアントから芋た時のDatagramPacket#getLengthが、サヌバから戻されたbyte配列の長さになりたす。

で、送信。

                (.send s p)))]

続いお、クラむアント偎。

udp_client.clj 
(import '(java.net DatagramPacket DatagramSocket InetSocketAddress))

(let [address (InetSocketAddress. 50000)]
  (with-open [socket (DatagramSocket.)]
    (let [word "Hello, Simple UDP Client!!"
          word-binary (.getBytes word "UTF-8")
          buffer (byte-array 8192)]
      (System/arraycopy word-binary 0 buffer 0 (alength word-binary))

      (let [packet (DatagramPacket. buffer 0 (alength word-binary) address)]
        (.send socket packet)
        (.setLength packet 8192)
        (.receive socket packet)
        (println (String. (.getData packet)
                          (.getOffset packet)
                          (.getLength packet)
                          "UTF-8"))))))

短いですよヌ。超ハマりたしたけど 。

クラむアント偎も、DatagramSocketを䜜成したす。

  (with-open [socket (DatagramSocket.)]

が、こちらはアドレス系の情報は指定したせん。ちょっずダミヌ的なずころがありたす。

ずりあえず、送信するデヌタを䜜成しお

    (let [word "Hello, Simple UDP Client!!"
          word-binary (.getBytes word "UTF-8")
          buffer (byte-array 8192)]
      (System/arraycopy word-binary 0 buffer 0 (alength word-binary))

送信。

      (let [packet (DatagramPacket. buffer 0 (alength word-binary) address)]

ここで、宛先のアドレスを指定しおいたす。

で、サヌバから戻っおくるデヌタは、送信したデヌタより長い可胜性があるずいうか、長いので、長さを広げおおきたす。

        (.setLength packet 8192)

あずは、DatagramSocket#receiveするだけですね。

        (.receive socket packet)

ここで、ちゃんず長さを考えないずいけないずか、DatagramPacketを䜜った時のbyte配列の長さにいろいろ䟝存するずかを最初わかっおなくお、いろいろハマりたした。たあ、冷静に考えるずそりゃあそうだよなぁっお感じですが。

぀か、受信ず送信でDatagramPacketを別にしおもよかったですかね。

受信したデヌタは、こちらもStringにしおいるだけです。

        (println (String. (.getData packet)
                          (.getOffset packet)
                          (.getLength packet)
                          "UTF-8"))))))

UDP゜ケットの勉匷には、こちらの曞籍を䜿っおいたす。

Javaネットワヌクプログラミングの真髄

Javaネットワヌクプログラミングの真髄

これから、頑匵っお勉匷したしょう。

で、自分の理解の確認ずClojureで曞いたずころの点怜の意味も含めお、Scalaで曞き盎しおいるのでこちらも貌っおおきたす。

行っおいるこずはほが同じなので、説明は割愛です。Clojure慣れしおいない人は、こちらの方がただ読みやすいかな 。

あ、クラむアントずサヌバは同じファむルに曞いおいたす。
UdpClientServer.scala

import java.net.{DatagramPacket, DatagramSocket, InetSocketAddress}
import java.nio.charset.StandardCharsets
import java.util.Date

import UdpHelper._

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

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

object UdpServer {
  def main(args: Array[String]): Unit = {
    val address = new InetSocketAddress(50000)
    for (socket <- new DatagramSocket(address)) {
      log("Simple Scala UDP Server", address, "Startup.")

      Iterator
        .continually {
          val packet = new DatagramPacket(Array.ofDim[Byte](8192), 8192)
          socket.receive(packet)
          packet
        }.foreach { packet =>
          log("Accept Client", "=>", packet.getSocketAddress)
          new Thread {
            override def run(): Unit = acceptedClient(socket, packet)
          }.start()
        }
    }
  }

  private def acceptedClient(socket: DatagramSocket, packet: DatagramPacket): Unit = {
    val word = new String(packet.getData,
                          packet.getOffset,
                          packet.getLength,
                          StandardCharsets.UTF_8)
    val replyWord = s"You Say => $word"
    val replyWordBinary = replyWord.getBytes(StandardCharsets.UTF_8)

    packet.setData(replyWordBinary, 0, replyWordBinary.size)
    socket.send(packet)
  }
}

object UdpClient {
  def main(args: Array[String]): Unit = {
    val address = new InetSocketAddress(50000)
    for (socket <- new DatagramSocket) {
      val word = "Hello, Simple UDP Client!!"
      val wordBinary = word.getBytes(StandardCharsets.UTF_8)
      val buffer = Array.ofDim[Byte](8192)

      System.arraycopy(wordBinary, 0, buffer, 0, wordBinary.size)

      val packet = new DatagramPacket(buffer, 0, wordBinary.size, address)
      socket.send(packet)
      packet.setLength(8192)
      socket.receive(packet)
      println(new String(packet.getData,
                         packet.getOffset,
                         packet.getLength,
                         StandardCharsets.UTF_8))
    }
  }
}