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.
Java Agile 鬮ォ�ェ陋滂ソス�ス�コ闕オ譁溷クキ�ケ譎「�ス�ウ驛「�ァ�ス�ュ驛「譎「�ス�ウ驛「�ァ�ス�ー