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

  • 22
    いいね
  • 1
    コメント
この記事は最終更新日から1年以上が経過しています。

メモ書きのため雑。

環境

  • Rails 5.0.0.beta1
  • Unity 5.3.0(C#)

概要

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

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

セットアップ

Railsのサーバについてはこちらの内容そのまんまでセットアップ。
Rails 5 + ActionCableで作る!シンプルなチャットアプリ(DHH氏のデモ動画より)

Unityでwebsocketするのにはwebsocket-sharpを使用し、MiniJSONを使ってJSONをエンコードする。
UnityでWebSocketを使用する
[Unity] MiniJSON 使って json 読み込み

サーバの起動

1つ気をつけなければいけないのが、websocket-sharpの実装の問題(たぶんこの部分)で、ActionCableをマウントして起動すると接続できない。
推測だけど、リダイレクトか、ヘッダーを厳密にチェックしすぎてるのが問題だと思われる。
そのため、ApplicationサーバとActionCable(websocket)サーバを別々に起動する。

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

ActionCableをStandaloneで起動する

公式のREADMEに書いてある方法で、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

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

bin/cable
#!/bin/bash
bundle exec puma -p 28080 cable/config.ru
$ 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を起動する

$ 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として登録される。

{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を使う際の強力な選択肢になりそう。