PlayFramework+WebSocketで送受信データを圧縮する

Play FrameworkではWebSocketのコントローラが受信する前/送信した後にFrameFormatterを使ってデータを加工する処理を入れることができるようになっています。今回はそれを用いて圧縮機能を入れてみました。使用したPlayのバージョンは2.2.1です。

FrameFormatterの使い方

PlayでWebSocketを使用する場合、以下のような感じでコントローラを書きます(Playのドキュメントから拝借)。

object Application extends Controller {
  def index =  WebSocket.using[String] { request =>
    //Concurernt.broadcast returns (Enumerator, Concurrent.Channel)
    val (out,channel) = Concurrent.broadcast[String]

    //log the message to stdout and send response back to client
    val in = Iteratee.foreach[String] { msg =>
        channel push("RESPONSE: " + msg)
    }
    (in,out)
  }
}

ここで使用しているWebSocket.using[A]は以下のように定義されてます。

def using[A](f: RequestHeader => (Iteratee[A, _], Enumerator[A]))(implicit frameFormatter: FrameFormatter[A]): WebSocket[A] = {
  WebSocket[A](h => (e, i) => { val (readIn, writeOut) = f(h); e |>> readIn; writeOut |>> i })
}

第2引数リストにFrameFormatter[A]が暗黙の引数で定義されています。 この第2引数リストのFrameFormatterに自分でカスタマイズしたものを渡せば色々できるようになります。 WebSocket.using[A]のタイプパラメータAにString, Arra[Byte], JsValue, Either[String, Array[Byte]]を渡して第2引数リストを省略するとPlayがデフォルトで用意したものが使われるようになっています。

独自の処理を行うFrameFormatterを作る

作り方は簡単でFrameFormatterのtransformメソッドを使用します。WebSocketから受け取るデータがテキストかバイナリかで使うFrameFormatterが異なります。バイナリの場合はWebSocket.FrameFormatter.byteArrayFrameを使い、文字列の場合はWebSocket.FrameFormatter.stringFrameを使用します。今回はzlibで圧縮されたものを送受信するのでFrameFormatter.byteArrayFrame.transformを使用します。 実際のコードは以下の用な感じです。エラー処理とかしてないのでだいぶ適当ですw。

object Application extends Controller {
  lazy val formatter: WebSocket.FrameFormatter[String] = WebSocket.FrameFormatter.byteArrayFrame.transform({
    sendString =>
      // 送信データの加工
      val bytes: Array[Byte] = sendString.getBytes("UTF-8")
      val compresser = new Deflater
      compresser.setInput(bytes)
      compresser.finish
      val buffer = new Array[Byte](512)
      val resultLength = compresser.deflate(buffer)
      val sendBytes = buffer.slice(0, resultLength)
      sendBytes
  }, {
    receivedBytes =>
      // 受信データの加工
      val decompresser = new Inflater
      decompresser.setInput(receivedBytes)
      val buffer = new Array[Byte](512)
      val resultLength = decompresser.inflate(buffer)
      decompresser.end
      new String(buffer, 0, resultLength, "UTF-8")
  })

  // WebSocket用のアクションはそのまま変わらず
  def echo = WebSocket.using[String] { request =>
    //Concurernt.broadcast returns (Enumerator, Concurrent.Channel)
    val (out,channel) = Concurrent.broadcast[String]

    //log the message to stdout and send response back to client
    val in = Iteratee.foreach[String] { msg =>
      channel push("RESPONSE: " + msg)
    }
    (in,out)
  }(formatter)
}

このように実際のアクションのコードを変更せずに送受信データの加工を行えるので便利です。 ちなみにPlayがデフォルトで用意しているJSONデータをやりとりするFrameFormatterは以下のように実装されています。

implicit val jsonFrame: FrameFormatter[JsValue] = stringFrame.transform(Json.stringify, Json.parse)

まとめ

今回はデータ圧縮機能を組み込んでみましたが、FrameFormatter.transformを使えばMessagePackなどPlayに用意されていないデータシリアライゼーションにもアクションのコードを変更せずに対応させることができるようになったりします。便利そうなので活用するといいことあるかもしれないですね。

Comments

comments powered by Disqus