WebSocketとは、ネットワーク用に定義された比較的新しい通信規格で、W3CとIETFが策定に関わっている、クライアント/サーバー間の双方向通信用技術規格です。
WebSocketはサーバーとクライアントが最初に接続を行った後、その後の通信を全てその接続で行います。その際にはHTTPより軽いプロトコルを使用するため、接続や通信のための負荷が低減されます。
WebSocketを使用すると、従来の双方向通信「XMLHttpRequest(Ajax)」「Comet」が持っていた欠点を解消できるといわれています。
なお、この記事ではWebSocketについての詳細な解説は行いません。WebSocketについてもっと詳しく知りたい方は、記事「node.jsの衝撃とWebSocketが拓く未来」などの記事をご覧ください。
Play2ではWebSocketを使用するために「play.api.mvc.WebSocket」オブジェクトが用意されています。この章では、Play2のGitHubにある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に関するドキュメントがあります。参考にしてみてください。
さて、今回はnginx連携やWebSocket/AkkaをPlay2アプリから使用する方法を紹介しました。最近はWebSocketといえばNode.jsのイメージが強いですが、Play2でもしっかり使用できるので、検討してみてください。
次回はPlay2のプラグインを紹介する予定です。
中村修太(なかむら しゅうた)
クラスメソッド勤務の新しもの好きプログラマです。昨年、東京から山口県に引っ越し、現在はノマドワーカーとして働いています。好きなJazzを聴きながらプログラミングするのが大好きです。
Copyright © ITmedia, Inc. All Rights Reserved.