ActionCable

WebSocket と ActionCable

More than 1 year has passed since last update.

  • 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用ラッパー といった出で立ち

https://github.com/faye/faye-websocket-ruby


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のお約束が通じなかったりするんで、頑張りましょう

終わり