メモ書きのため雑。
環境
- 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を別々に起動する。
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!
require 'action_cable/process/logging'
run ActionCable.server
また、APIとしてActionCableを利用する場合は下記の設定をする必要がある
ActionCable.server.config.disable_request_forgery_protection = true
起動スクリプトを作成して起動する。
#!/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
を変更する。
@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
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の必要がある。
class Cable.Subscription
constructor: (@subscriptions, params = {}, mixin) ->
@identifier = JSON.stringify(params)
ActionCableのクライアント側実装はこのようになってる。
C#から送る場合であれば、一度DictionaryをJSONの文字列にデコードしてからパラメータにセットする。
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を指定する。
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はサーバ側のメソッド名。
def speak(data)
Message.create! content: data['message']
end
これでUnity(C#)のクライアント側からデータを送信できた。
感想
当たり前だけどまだほとんどドキュメントがなかったからかなりコードを読む必要があったけれど、ActionCableはコードが読みやすいのであまり苦にならなかった。
(websocket-railsを採用しなかったのは、ActionCableを使ってみたかったのもあるけどコードがかなり読みにくそうだったため)
WebSocketサーバがRailsEngineとして実装されているためApplicationサーバと疎結合な作りになっていたり、pubsubにRedisを利用しているなどスケールアウトするときも簡単そうで、ActiveJobを使うことで楽に非同期実行が可能など、websocketを使う際の強力な選択肢になりそう。