序
- Rails5 Meetup 発表資料
はじめに
- 学生の頃に Socket.IO でゲームを作ってた
- Rails は業務でコントローラに API 生やす程度
- rspec が全然わからん
無茶振り
yuku 「mizchi なら ActionCableでなんか作れるでしょ」
なんか作った
今日の発表内容
- WebSocket の現状
- ActionCable
既存機能のRails5の拡張については @takashi に任せる
1. WebSocket
WebSocketとは
- Webブラウザで扱えるTCP Socket抽象
- HTTP1.1と比べて並列/高頻度イベントの効率が良い
- プッシュ配信
今までWebSocket が使えなかった背景
昔話
- 未対応ブラウザが多すぎて、フォールバック必要
- まともな Fallback は、ほぼ Socket.IO の特権
- ロードバランサが辛い
- 二度目以降のリクエストを、必ず最初に接続した場所に戻す必要
フォールバック先
- Commet
- XHL Long Polling
いずれも Performance に難
今
- フォールバック不要(IE>=11)
- AWS ALB
- Nginx のWebSocket サポート
- Heroku の WebSocket サポート(※近くの Region がない)
競合するスペック
- WebRTC の P2P
- HTTP/2のリクエスト多重化
- ServiceWorker はバックグラウンドでもプッシュ通知を扱う
=> 用途を選びましょう
どこで使う?
- チャットサーバー
- MMORPGのようなゲーム
- SNS でのリアルタイムイベント通知
- 高頻度な時系列データのビジュアライズ
2 ActionCable
ActionCable
- 中身は FayeWebsocket の Rails用ラッパー といった出で立ち
ActionCable のメリット
- シームレスなRailsインテグレーション
- Rails::Engine をマウントする実装なので、WebSocketサーバーを切り離せる
- 比較的枯れたRails運用ノウハウがそのまま使える
ActionCable を使うには
- Unicorn と EventMachine が相性悪いため、Puma必須
- Rails 側で Channel を作成
- JS 側 で 任意の Channel を 購読する
Channel
- 購読される単位
- チャットアプリならば1つのチャットルームに相当
-
bloadcast
でChannel 購読者全員にプッシュ -
bloadcast_to
特定のユーザーにプッシュ
実例: JS から ActionCable への接続
application.js
// sprockets
//= aciton_cable
var cable = ActionCable.createConsumer("/cable");
or
main.js
// babel/browserify env
// npm install actioncable
import ActionCable from "actioncable";
const cable = ActionCable.createConsumer(/"cable");
引数で任意のエンドポイントを取れる
実例: チャンネル名を指定して接続
app/channels/chat_room.rb
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room_id]}"
end
end
application.js
cable.subscriptions.create({
channel: "ChatChannel",
room_id: "my-chat-room"
}, {
connected: function() {
console.log("connected");
}
// ...
});
実例: クライアントからイベントを投げる
main.js
const subscription = cable.subscriptions.create("ChatChannel", {/* callback */});
subscription.perform("foo", {data: 1})
app/channels/chat_room.rb
class ChatChannel < ApplicationCable::Channel
//...
def foo(data)
puts data
end
end
perform(action, data)
を実行すると ChatChannel[:action]
が呼ばれる。
bloadcast と received
app/channels/chat_room.rb
class ChatChannel < ApplicationCable::Channel
//...
def foo(data)
ActionCable.server.broadcast "ChatChannel", data
end
end
ChatChannel
の購読者全員に data をプッシュする
cable.subscriptions.create("ChatChannel", {
// ...
received(data) {
console.log(data) // => {data: 1, action: "foo"}
}
});
bloadcast_to
でユーザー個別に送ることもできる
デモ
本当は作りたかったもの
- 編集が1s止まったら入力を送信(ここまで実装済み)
- 3 way merge
- 他人のチャンク編集後、他人のチャンク編集前、自分の状態
- 更新後のカーソル位置補正
ActionCable の学習資料
閑話休題: Elixir/Phoenix
- Phoenix.Channel は ActionCable 丸パクリ(作者はRailsコミッタの Chris McCord)
- プロセスモデル/応答性の観点からはPhoenixの方が WebSocket に向いてそう
翻訳: 似て非なる Phoenix と Rails(原題『Phoenix is not Rails』) - Qiita
設計を考える
WebSocket アプリケーションの設計
- 高頻度イベントにまつわる状態は Redis が向いてる
- DBに 永続化するのは何らかのチェックポイントを通った時
- 状態の差分を更新し続けるのは難易度が高いので、定期的に state を全部渡してsyncする、などの保証は欲しい
チャットの場合
- 最初のユーザーが部屋にログイン
- => 部屋名をキーにRedisのリスト要素を作成
- 誰かが発言イベントを投げる
- => リスト要素を更新
- => 発言を bloadcast
- 1分に1回
- => 全員に現在のリストを配信して補正
- 5分に1回
- => リスト要素の中身を DB に保存
- 最後のユーザーがログアウト
- => リスト要素の中身を DB に保存
- => Redisの要素を削除
設計意図
-
できるだけ DB を触らず Redis のインメモリデータだけ使う
- Redisは肥大化すると運用が辛いので、使い終わったらすぐ消す
- Redisの スナップショット を DB に保存する
-
ここまで来ると、あとは オンラインゲームを支える技術 などが参考になる
趣味でオンラインゲーム作ってた時
- ダンジョン階層ごとにチャンネル分割
- 12FPSでゲームステートを配信
- クライアントは60FPSなので、時系列データから他のEntityの座標予測
- モンスターを倒したら保存
最後に
- まだ負荷計測に至ってないので、どこがボトルネックになるかわかってない
- 高頻度イベントはWebのお約束が通じなかったりするんで、頑張りましょう