Play2+nginx/Akka/WebSocketで高速双方向通信Scala+Play 2.0でWebアプリ開発入門(11)(2/2 ページ)

» 2013年12月18日 18時00分 公開
[中村修太,クラスメソッド株式会社]
前のページへ 1|2       

Play2アプリでWebSocketを使う

WebSocketとは

 WebSocketとは、ネットワーク用に定義された比較的新しい通信規格で、W3CIETFが策定に関わっている、クライアント/サーバー間の双方向通信用技術規格です。

 WebSocketはサーバーとクライアントが最初に接続を行った後、その後の通信を全てその接続で行います。その際にはHTTPより軽いプロトコルを使用するため、接続や通信のための負荷が低減されます。

 WebSocketを使用すると、従来の双方向通信「XMLHttpRequest(Ajax)」「Comet」が持っていた欠点を解消できるといわれています。

 なお、この記事ではWebSocketについての詳細な解説は行いません。WebSocketについてもっと詳しく知りたい方は、記事「node.jsの衝撃とWebSocketが拓く未来」などの記事をご覧ください。

 Play2ではWebSocketを使用するために「play.api.mvc.WebSocket」オブジェクトが用意されています。この章では、Play2のGitHubにあるWebSocketを使用したチャットのサンプルをベースに、チャット機能を実装してみましょう。

Play2アプリからWebSocketを使ってみる

 ではチャット機能を実装していきましょう。これもgyroプロジェクトに機能を追加していきます。

 まずは「conf/routes」ファイルに、チャット用の新しいコントローラーを登録します。

GET		/room/:nickName             controllers.ChatController.showRoom(nickName:String)
GET		/room/socket/:nickName    	controllers.ChatController.chatSocket(nickName:String)

 最初のroute定義ではチャット画面初期表示、2番目のroute定義でWebSocketの接続を行うためのURL定義を行います。

 次に、app/controllersディレクトリにChatController.scalaファイルを作成し、その中に次のような定義を実装します。

package controllers
import akka.actor._
import akka.pattern.ask
import akka.util.Timeout
import play.api.libs.iteratee._
import play.api.libs.concurrent._
import play.api.mvc.WebSocket
import play.api.Play.current
import play.api.mvc.Controller
import play.api.mvc.Action
object ChatController extends Controller {
  implicit val timeout = Timeout(1)
  val room = Akka.system.actorOf(Props[ChatRoom])
  
  def showRoom(nickName: String) = Action { implicit request =>
    Ok(views.html.chat(nickName))
  }
  
  def chatSocket(nickName: String) = WebSocket.async { request =>
    val channelsFuture = room ? Join(nickName)
    channelsFuture.mapTo[(Iteratee[String, _], Enumerator[String])]
  }
}

 このチャットサンプルでは、Akkaを使用してチャット機能を実装しています。WebSocket通信をするためのchatSocket関数を見てみましょう。

 普通のHTTPリクエストを処理するためにはActionを使っていましたが、WebSocketリクエストを処理するためには、ActionではなくWebSocketを使います。そして、WebSocketはinとoutの2つのチャンネルを返す必要があります。

 inチャンネル(Iteratee[String, _])は、Iteratee[A,Unit]型(Aはメッセージの型)で、メッセージを受信するたびに通知を受け取ります。tチャンネル(Enumerator[String])はEnumerator[A]型で、クライアントへ送信するメッセージを生成します。

 このチャンネルにEOFを送信することで、通信をサーバー側から切断できます。chatSocket関数ではどちらもString型を指定していますね。

 例えば次のサンプルコードでは、受信した各メッセージを出力するだけのIterateeとメッセージを送信するためのEnumeratorを作成しています。

・
・
def sampleCode = WebSocket.using[String] { request => 
  // in channel
  val in = Iteratee.foreach[String](println).mapDone { _ =>
    // クライアント切断時の処理
    println("Disconnected")
  }
  
  // out channel
  val out = Enumerator("Hello WebSocket")
  
  (in, out)
}

 次に、Actorでメッセージ種類を判定するためのcase classを定義しておきます。

case class Join(nickName: String)
case class Leave(nickName: String)
case class Broadcast(msg: String)

 そして、Controllerから利用するActorを定義します。下記アクターのreceiveはWebSocketからメッセージを受け取るたびに処理されます。

class ChatRoom extends Actor {
  var users = Set[String]()
  val (enumerator, channel) = Concurrent.broadcast[String]
  def receive = {
    case Join(nickName) => {
      if (!users.contains(nick)) {
        val iteratee = Iteratee.foreach[String] { message =>
          self ! Broadcast("%s: %s" format (nickName, message))
        }.mapDone { _ =>
          self ! Leave(nickName)
        }
        users += nickName
        channel.push("User %s has joined the room, now %s users"
          format (nickName, users.size))
        sender ! (iteratee, enumerator)
      } else {
        val enumerator = Enumerator(
          "Nickname %s is already in use." format nickName)
        val iteratee = Iteratee.ignore
        sender ! (iteratee, enumerator)
      }
    }
    case Leave(nickName) => {
      users -= nickName
      channel.push("User %s has left the room, %s users left"
        format (nickName, users.size))
    }
    case Broadcast(msg: String) => channel.push(msg)
  }
}

 Join/Leave/Broadcast、それぞれメッセージを受け取ると、クライアントにその処理に応じたString文字列を返します。最後に、「views/chat.scala.html」を作成しましょう。

@(nickName: String)(implicit request: RequestHeader)
@main("Chatroom for " + nickName) {
  <h1>Chatroom - You are @nickName</h1>
    <form id="chatform">
    <input id="text" placeholder="Say something..." />
    <button type="submit">Say</button>
  </form>
  <ul id="messages"></ul>
  <script type="text/javascript">
  $(function() {
    ws = new WebSocket("@routes.ChatController.chatSocket(nickName).webSocketURL()");
    //ws = new WebSocket("ws://localhost:9000/room/socket/{nickName}")になる
    ws.onmessage = function(msg) {
      $('<li />').text(msg.data).appendTo('#messages')
	}
    $('#chatform').submit(function(){
      ws.send($('#text').val())
      $('#text').val("").focus()
      return false;
	}); 
  });
  </script> 
}

 クライアント側では普通にWebSocket接続を行っています。「@routes.ChatController〜」と、webSocketURL関数を使用して記述していますが、このように記述すると、指定したWebSocket用関数に接続するためのURLを逆引きしてくれます。

 そして、実際にonmessageでメッセージを受け取ると、会話メッセージが追加されていきます。

 ではアプリを起動して動作を確認してみましょう。

% cd /path/your/apppath
% play 
・
・
・
[gyro] % run

 以下のようにURLを入力して、複数ブラウザーでアクセスしてメッセージを送ってみてください。

http://localhost:9000/room/{ニックネーム}

 メッセージを送ると、全てのブラウザーでメッセージが表示されます。

 Play2の公式ページにもWebSocketに関するドキュメントがあります。参考にしてみてください。

次回はPlay2のプラグイン

 さて、今回はnginx連携やWebSocket/AkkaをPlay2アプリから使用する方法を紹介しました。最近はWebSocketといえばNode.jsのイメージが強いですが、Play2でもしっかり使用できるので、検討してみてください。

 次回はPlay2のプラグインを紹介する予定です。

著者プロフィール

中村修太

中村修太(なかむら しゅうた)

クラスメソッド勤務の新しもの好きプログラマです。昨年、東京から山口県に引っ越し、現在はノマドワーカーとして働いています。好きなJazzを聴きながらプログラミングするのが大好きです。


前のページへ 1|2       

Copyright © ITmedia, Inc. All Rights Reserved.

RSSについて

アイティメディアIDについて

メールマガジン登録

@ITのメールマガジンは、 もちろん、すべて無料です。ぜひメールマガジンをご購読ください。