Edited at

WebSocket と ActionCable

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



終わり