Node.js vs Play framework 第二弾 ( websocket 編 )

今回のネタはNodeとPlay比較ネタの第二弾です。
ちなみに第一弾はこちら

だいぶ前に @sugyan さんのエントリーでSocket.IOがどれくらいリアルタイムなのかちょっと計ってみた - すぎゃーんメモというものがあったので、これの1クライアントで計測する簡易版を Play 2.0 で作成して、さくらインターネットの VPS 上で計測してみました。

測定方法


概要


クライアント側で現在日時を表すデータを作成し、サーバー側へ送信。
サーバー側ではそれをそのままJSON型のオブジェクトに格納しなおしてクライアントへ再送信。
クライアント側で受信した時の日時とサーバーからもらった日時を比較して、どれくらい遅延したのかを出す、
というやり方で測定しています。

実装内容


Application.scala

package controllers

import akka.actor._
import akka.pattern.ask
import akka.util.duration._
import akka.util.Timeout

import play.api._
import play.api.libs.json._
import play.api.libs.iteratee._
import play.api.libs.concurrent._
import play.api.mvc.WebSocket
import play.api.Play.current

object Application extends {
  def index = WebSocket.async[JsValue] {
    request  => get()
  }
  implicit val timeout = Timeout(1 second)
  lazy val actor = {
    val timeActor = Akka.system.actorOf(Props[TimeHolder])
    timeActor
  }
  def get():Promise[(Iteratee[JsValue,_],Enumerator[JsValue])] = {
    (actor ? Init()).asPromise.map {
      case Connected(enumerator) => 
        val iteratee = Iteratee.foreach[JsValue] { event =>
          actor ! Get(event \  "datetime")
        }.mapDone { _ =>
          println("Disconnected.")
        }
        (iteratee,enumerator)
    }

  }

class TimeHolder extends Actor {
  var data: PushEnumerator[JsValue] = Enumerator.imperative[JsValue]( onStart = self )
  def receive = {
    case Init() => {
      sender ! Connected(data)
    }
    case Get(time) => {
      data.push(writes(time))
    }
  }
  def writes(data: JsValue): JsValue = JsObject(Seq(
	"message" -> JsString("pong"),
	"datetime" -> data
  ))
}

case class Init()
case class Get(time: JsValue)
case class Connected(enumerator:Enumerator[JsValue])
}

websocket.html

<html>
    <head>
        <title>test</title>
        <script type="text/javascript" charset="utf-8">
    
        var WS = window['MozWebSocket'] ? MozWebSocket : WebSocket
        var socket = new WS("ws://localhost:9000/")
        socket.onopen = function(evt) {
          socket.send(JSON.stringify(
          {message:'ping', datetime: new Date().getTime()}
          ))
        };
        socket.onmessage = function(evt) { onMessage(evt) };

          function onMessage(evt) {
            var data = JSON.parse(evt.data)
            var delay = new Date().getTime() - data.datetime;
            console.log('pong: ' + delay + ' ms ');
            socket.close();
        }
        
    
        </script>
    </head>
    <body>
    </body>
</html>

Node側


Nodeは @sugyan さんの実装と特に変えていません。1 クライアントで計測しているので、broadcastをやめたことくらいですかね。

var server = require('http').createServer(function (req, res) {
    require('data-section').get('html', function (err, data) {
        res.writeHead(200, { 'Content-Type': 'text/html' });
        res.end(data);
    });
});
var io = require('socket.io').listen(server);
// io.set('transports', ['xhr-polling']);
io.sockets.on('connection', function (socket) {
    socket.on('ping', function (data) {
        socket.emit('pong', {
            clients: Object.keys(io.sockets.clients()).length,
            data: data
        });
    });
});
server.listen(3000);

/*__DATA__
@@ html
<!DOCTYPE html>
<html>
  <head>
    <title>test</title>
    <script type="text/javascript" src="/socket.io/socket.io.js"></script>
    <script type="text/javascript">
var start = new Date().getTime();
var socket = io.connect();
var ua = window.navigator.userAgent;
socket.on('pong', function (data) {
  var delay = new Date().getTime() - data.data.datetime;
  if (data.data.ua === ua) {
    console.log('pong: ' + delay + ' ms on ' + data.clients + ' clients');
  }
});
socket.on('connect', function () {
  console.log('connect: ' + (new Date().getTime() - start));
    socket.emit('ping', {
      datetime: new Date().getTime(),
      ua: ua
    });
});
    </script>
  </head>
  <body>
  </body>
</html>
__DATA__*/


測定環境


さくらインターネット 1G VPS
Play側:
Play 2.0
Java 1.7.0_u3

Node 側:
Node.js 0.7.2
socket.io 0.9.5

結果

測定方法 Node.js + socket.io Play 2.0 + websocket
起動直後レスポンス 45ms 97ms
平均レスポンス 25.4ms 25.0ms

最初のブラウザからの読み込み時にPlayは必ずScalaのコンパイルが入るので、多少遅くなりました。
しかし、その後は双方あまり変わらない結果になりました。

ある程度負荷をかけないと変わらないかも、と思ったので、 fibonacci数列を解かせるように
途中に組み込んで実行してみました。

測定方法 Node.js + socket.io Play 2.0 + websocket
起動直後レスポンス 45ms 97ms
平均レスポンス 25.4ms 25.0ms
平均レスポンス(fibonacci(40)) 1990.4ms 404.8ms

少しNodeに不利すぎた条件だったのかもしれませんが、双方でかなり差がでました。
やはりこういうCPUに負荷のかかる処理はスクリプト言語よりもコンパイラ型言語ですね。

Playは内部的に大規模なコンパイルをしている分、初期動作はNodeよりも劣るみたいです。
ただ、CPUに負荷がかかる処理をする場合は Play の方が Node に勝ります。
純粋な websocket 処理時間だけで言えば同等と言えそうです。

なんとなく当然といえば当然の結果か。。。

まだ1クライアントでしか計測していないのと、Node側は複数クラスタ、Isolatesとかを試してチューニングしてみるのも
よさそうです。Play側は Javaと比較してみるのも良いかもしれませんね。

感想


やっぱりsocket.ioみたいな簡単に使えるwebsocketのツールがPlayにも欲しいですね。結構Play 2.0 の Scala で Websocketを
使うのは Iteratee, Enumerator等の概念を覚えなきゃいけなくて、はじめの一歩が遠いです。
※まだ使いこなせている感じがしない・・・。