PlayframeworkでWebSocketを使う

この記事は[Playframework Advent Calendar](http://www.adventar.org/calendars/556)の15日目の記事です。

2014年は個人的にWebSocketブームでした。  
春に[quizar](http://www.quizar.info/)というクイズゲームを作ったのを皮切りにいくつかのWebSocketアプリを作りましたが、途中からはWebSocket周りを分離してフレームワーク化していました。

これまではずっとgit submoduleでソースをインポートして、アプリとフレームワークの両方をメンテしながら作ってましたが、APIも固まってきたので一度使い方を整理しておこうと思います。

ていうか、先日久しぶりに新規アプリで使ったら導入あたりの手順を思いっきり忘れていたので主に**自分用のメモ**です。(^^;

ちなみにこのライブラリを使用した最新のアプリはこれです。(2人用対戦ゲーム)

http://salesforce-vs.herokuapp.com/

## GitHub
クライアント用のJavaScriptとPlayframework用のライブラリの二つのリポジトリを使っています。

https://github.com/shunjikonishi/roomframework
https://github.com/shunjikonishi/roomframework-play

Play用のリポジトリのディレクトリ構成が標準から外れているのはずっとgit submoduleで開発していた名残です。

## インストール
クライアント

``` bash
bower install roomframework
```

dist/roomframework[.min].jsを任意のディレクトリにコピーします。

サーバ
現在はFLECTがGitHub上に公開しているmaven-repoにホストしています。


``` build.sbt
libraryDependencies ++= Seq(
  cache,
  "roomframework" %% "roomframework" % "0.9.5"
)

resolvers ++= Seq(
  "FLECT Maven Repository on Github" at "http://flect.github.io/maven-repo/"
)
```

対応しているPlayバージョンは2.3.0以降(scalaVersion 10.4, 11.1)です。  
2.2系でもgit submoduleでソースをインポートすれば多分動きます。

## 使い方 - クライアント
jQueryに依存してます。

``` javascript:インスタンス生成
var con = new room.Connection("ws://localhost:9000/ws");

//または

var con = new room.Connection({
    url: "ws://localhost:9000/ws",
    logger: console,
    authToken: "xxxx"
});
```

コンストラクタの引数はws接続のURL文字列またはurlを含むハッシュです。  
引数のハッシュで使えるキーはいくつかありますが、良く使うのはloggerとauthTokenの二つ。

loggerにconsoleを渡せば全通信がconsole.logに出力されます。(logメソッドを持っていれば他のオブジェクトも渡せます。)

authTokenはログイン後にのみ接続を許可するアプリで使用するトークンですが今回は説明を割愛します。


``` javascript:メッセージ送信
con.request({
    command: "command1",
    data: {
        key1: "hoge",
        key2: 5,
        key3: {
            key4: true
        }
    },
    success: function(data) {
        ....
    },
    error: function(data) {
        ....
    }
```

command名とdataを指定して送信。dataには任意のハッシュ、string, numberなど何でも指定できます。
リクエストに対して同期的にレスポンスが返ってくる場合はそれを、successまたはerror関数でハンドルできます。
レスポンスが無い、またはハンドルする必要が無い場合はsuccess, errorは不要です。

``` javascript:サーバからのメッセージ受信
con.on("command2", function(data) {
    ...
});
```

コマンド名とそれに対応するイベントハンドラをonメソッドで登録します。
第3引数にエラーハンドル用のfunctionを設定することもできますが、あんまり使ったことないです。

con.offでイベントハンドラの解除ができますが、これもほとんど使うことはありません。

## 使い方 - サーバ(接続ユーザとの応答でのみ使用する場合)
WebSocketをクライアントとの1対1通信でのみ使う場合の使い方です。
Ajaxの代替として使うことができます。

やることは以下の2つ

- CommandInvokerのサブクラスを実装する
- ControllerにWebSocket用のハンドラを実装する

CommandHandlerの実装は以下のような感じ

``` scala
class MyCommandHandler extends CommandHandler {
  def init = {
    //処理を行ってレスポンスを返す
    addHandler("command1") { command =>
      val key1 = (command.data \ "key1").as[String]
      val key2 = (command.data \ "key2").asOpt[String]
      ...
      val responseData: JsValue = ...
      command.json(responseData)
    }
    //処理を行うが何もレスポンスは返さない
    addHandler("command2") { command =>
      val key1 = (command.data \ "key1").as[String]
      val key2 = (command.data \ "key2").asOpt[String]
      ...
      CommandResponse.None
    }
    //定期的にサーバからメッセージを送信する
    Akka.system.scheduler.schedule(0 seconds, 60 seconds) {
      val responseData: JsValue = ...
      send(new CommandResponse("serverCommand", responseData)
    }
  }
  init
}

```

ポイントは以下です。

- メッセージの送受信にはCommandクラスとCommandResponseクラスを使用する
- addHandlerでクライアントからのリクエストハンドラを登録する
- サーバからメッセージを送信する場合はsend(CommandResponse)を使用する

CommandHandlerが実装できたらそれを使用するControllerメソッドを実装します。
こちらはほぼワンパターンで以下のようになります。

``` scala
def ws = WebSocket.using[String] { request =>
  val h = new MyCommandHandler
  (h.in, h.out)
}
```

## 使い方 - サーバ(ルーム内に複数ユーザを入れる場合)
Roomを使うと複数のユーザにメッセージをブロードキャストするようなアプリを作成することができます。
このモデルの典型的なアプリケーションはチャットです。

やることは以下の3つ

- RoomHandlerのサブクラスを実装する
- シングルトンのRoomManagerを作る
- ControllerにWebSocket用のハンドラを実装する

RoomHandlerはCommandInvokerのサブクラスなので基本的な使い方は同じです。
拡張されている点は

- 全ユーザにメッセージを送信するbroadcast(CommandResponse)メソッドがある
- CommandFilterを使用してbroadcastメッセージをフィルタできる

の2点です。
以下はルーム内の全ユーザで共有するチャットと指定のユーザにのみダイレクトメッセージを送信する実装例です。

``` scala
class MyRoomHandler(room: Room, username: String) 
  extends RoomHandler(room) with CommandFilter
{
  def init = {
    addHandler("chat") { command =>
      broadcast(command.json(command.data))
      CommandResponse.None
    }
    addHandler("directMessage") { command =>
      broadcast(command.json(command.data))
      CommandResponse.None
    }
  }
    
  def filter(msg: CommandResponse): Option[CommandResponse] = {
    if (msg.name == "directMessage" && (msg.data \ "username") != username) {
      None
    } else {
      Some(msg)
    }
  }
  init
}
```

特定ユーザにのみメッセージを送信する場合も一度broadcastしてそれを受信したハンドラ側でフィルタリングしています。

RoomManagerは通常はobjectとしてRoomManagerを作成すればOKです。

``` scala
object MyRoomManager extends RoomManager(new DefaultRoomFactory())

```

DefaultRoomFactoryの代わりにRedisRoomFactoryを使用するとbroadcast時にredisを経由するようになります。(つまりスケール可能になります。)

Controllerの実装は以下のようになります。

``` scala
def ws(roomname: String, username: String) = WebSocket.using[String] { request =>
  val room = MyRoomManager.join(roomname)
  val h = new MyRoomHandler(room, username)
  (h.in, h.out)
}
```

## 使い方 - サーバ(ルームを拡張する場合)
Roomで状態を管理したい場合は

- Roomのサブクラスを実装する
- 上記のRoomを返すRoomFactoryを実装する

という流れになります。
Roomの操作は複数のユーザ(スレッド)が同時並行的に行う可能性があるのでActorを経由するようにした方が良いです。

具体的な実装例は

https://github.com/shunjikonishi/programming-game/blob/master/app/models/GameRoom.scala

を参考にしてください