ActionCableをwebsocket APIとして使ってUnityと通信する


メモ書きのため雑。

# 環境

- Rails 5.0.0.beta1
- Unity 5.3.0（C#）

## 概要

RailsとUnityでwebsocketを使った通信が出来るか試してみた。
たぶん、同じやり方でC#以外の言語からでも通信できると思う。

※Unityと言いつつ、C#の内容が100%です。

## セットアップ

Railsのサーバについてはこちらの内容そのまんまでセットアップ。
[Rails 5 + ActionCableで作る！シンプルなチャットアプリ（DHH氏のデモ動画より）](http://qiita.com/jnchito/items/aec75fab42804287d71b)

Unityでwebsocketするのにはwebsocket-sharpを使用し、MiniJSONを使ってJSONをエンコードする。
[UnityでWebSocketを使用する](http://qiita.com/oishihiroaki/items/bb2977c72052f5dd5bd9)
[[Unity] MiniJSON 使って json 読み込み](http://qiita.com/phi/items/914bc839b543988fc0ec)

## サーバの起動

１つ気をつけなければいけないのが、websocket-sharpの実装の問題（たぶん[この部分](https://github.com/sta/websocket-sharp/blob/master/websocket-sharp/WebSocket.cs#L717-L734)）で、ActionCableをマウントして起動すると接続できない。
推測だけど、リダイレクトか、ヘッダーを厳密にチェックしすぎてるのが問題だと思われる。
そのため、ApplicationサーバとActionCable（websocket）サーバを別々に起動する。

（他の言語やライブラリなら問題なく接続できるものもありそうなので試してみてダメそうならこのやり方で）

### ActionCableをStandaloneで起動する

[公式のREADME](https://github.com/rails/rails/tree/master/actioncable#standalone)に書いてある方法で、RailsとActionCableを別々に起動する。

```cable/config.ru
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!

require 'action_cable/process/logging'

run ActionCable.server
```

**また、APIとしてActionCableを利用する場合は下記の設定をする必要がある**

```config/initializer/action_cable.rb
ActionCable.server.config.disable_request_forgery_protection = true
```

起動スクリプトを作成して起動する。

```sh:bin/cable
#!/bin/bash
bundle exec puma -p 28080 cable/config.ru
```

```sh
$ chmod +x bin/cable
$ bin/cable
```

### RailsでActionCableの参照URLを指定する

別々に起動した場合、rails側からのActionCableの参照URLを指定する必要がある。
`assets/javascripts/cable.coffee` を変更する。

```assets/javascripts/cable.coffee
@App ||= {}
App.cable = ActionCable.createConsumer("ws://localhost:28080") #=> ActionCable側のポートを指定
```

railsを起動する

```sh
$ bin/rails s
```

この設定を変更することで、スケールしたい場合などにActionCableとRailsを別々のサーバに割り当てることも可能っぽい。


## クライアントからのSubscribe
### ActionCableのクライアント側の実装
クライアント側からイベントを購読するには、"command"のパラメータに"subscribe"を渡して送信する。
https://github.com/rails/actioncable/blob/0d99cfd5ca755643a23beec22513072987c8edba/lib/assets/javascripts/cable/subscriptions.coffee#L21-L24

```assets/javascripts/cable/subscriptions.coffee
create: (channelName, mixin) ->
  channel = channelName
  params = if typeof channel is "object" then channel else {channel}
  new Cable.Subscription this, params, mixin

# ...

add: (subscription) ->
  @subscriptions.push(subscription)
  @notify(subscription, "initialized")
  @sendCommand(subscription, "subscribe")

# ...

sendCommand: (subscription, command) ->
  {identifier} = subscription
  if identifier is Cable.PING_IDENTIFIER
    @consumer.connection.isOpen()
  else
    @consumer.send({command, identifier})

```

identifierはルームを表す識別子で、 `create` メソッドで `Cable.Subscription` に渡されている `params` がここに渡される。
つまり今回の例だと、 `{channel: "RoomChannel"}` となる。

### APIとして利用する場合のクライアント側の実装

上記の例から、websocketで接続した後に下記のようなJSONを送信すればsubscriberとして登録される。

```json
{command: "subscribe", identifier: "{\"channel\": \"RoomChannel\"}"}
```

identifierの中身はHashでなく、stringの必要がある。

```assets/javascripts/cable/subscription.coffee
class Cable.Subscription
  constructor: (@subscriptions, params = {}, mixin) ->
    @identifier = JSON.stringify(params)
```

ActionCableのクライアント側実装はこのようになってる。
C#から送る場合であれば、一度DictionaryをJSONの文字列にデコードしてからパラメータにセットする。

```websocket.cs
ws.OnOpen += (sender, e) => {
	var dict = new Dictionary<string, object>();
	var identifier = new Dictionary<string, string> ();
	identifier["channel"] = "RoomChannel";

	dict["command"] = "subscribe";
	dict["identifier"] = Json.Serialize(identifier); // 一度JSONにシリアライズしておく
	ws.Send(JSON.Serialize(dict));
};
```

これでUnity（C#）のクライアント側から"RoomChannel"の購読が開始される。

## メッセージの送信

ここまで来たらメッセージの送信も簡単。
メッセージを送信する場合は、"command"を"message"にして、"data"にJSONデコードした文字列とサーバ側のactionを指定する。

```websocket.cs
var dict = new Dictionary<string, object>();
var identifier = new Dictionary<string, string> ();
var data = new Dictionary<string, object>();

identifier["channel"] = "RoomChannel";

data["message"] = "foo";
data["action"] = "speak";

dict["command"] = "message";
dict["identifier"] = Json.Serialize(identifier);
dict["data"] = Json.Serialize(data);
		
ws.Send (Json.Serialize(dict));
```

actionはサーバ側のメソッド名。

```channel/room_channel.rb
  def speak(data)
    Message.create! content: data['message']
  end
```

これでUnity（C#）のクライアント側からデータを送信できた。

## 感想

当たり前だけどまだほとんどドキュメントがなかったからかなりコードを読む必要があったけれど、ActionCableはコードが読みやすいのであまり苦にならなかった。
（websocket-railsを採用しなかったのは、ActionCableを使ってみたかったのもあるけどコードがかなり読みにくそうだったため）

WebSocketサーバがRailsEngineとして実装されているためApplicationサーバと疎結合な作りになっていたり、pubsubにRedisを利用しているなどスケールアウトするときも簡単そうで、ActiveJobを使うことで楽に非同期実行が可能など、websocketを使う際の強力な選択肢になりそう。