WebSocket と ActionCable

## 序

- Rails5 Meetup 発表資料

-----

## はじめに

- 学生の頃に Socket.IO でゲームを作ってた
- Rails は業務でコントローラに API 生やす程度
- rspec が全然わからん

---

## 無茶振り

yuku 「mizchi なら ActionCableでなんか作れるでしょ」

---

## なんか作った

---

## 今日の発表内容

- WebSocket の現状
- ActionCable

既存機能のRails5の拡張については @takashi に任せる

---

# 1. WebSocket

---

## WebSocketとは

- Webブラウザで扱えるTCP Socket抽象
- HTTP1.1と比べて並列/高頻度イベントの効率が良い
- プッシュ配信

---

## 今までWebSocket が使えなかった背景

---

![](https://i.gyazo.com/5413db627659f497e22b593b56586257.png)

---

## 昔話

- 未対応ブラウザが多すぎて、フォールバック必要
  - まともな 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 への接続

```js:application.js
// sprockets
//= aciton_cable
var cable = ActionCable.createConsumer("/cable");
```

or 

```js:main.js
// babel/browserify env
// npm install actioncable
import ActionCable from "actioncable";
const cable = ActionCable.createConsumer(/"cable");
```

引数で任意のエンドポイントを取れる

---

## 実例: チャンネル名を指定して接続

```ruby:app/channels/chat_room.rb
class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room_id]}"
  end
end
```

```js:application.js
cable.subscriptions.create({
   channel: "ChatChannel",
   room_id: "my-chat-room"
}, {
  connected: function() {
    console.log("connected");
  }
  // ...
});
```

---

## 実例: クライアントからイベントを投げる

```js:main.js
const subscription = cable.subscriptions.create("ChatChannel", {/* callback */});

subscription.perform("foo", {data: 1})
```


```ruby:app/channels/chat_room.rb
class ChatChannel < ApplicationCable::Channel
  //...
  def foo(data)
    puts data
  end
end
```

`perform(action, data)` を実行すると `ChatChannel[:action]` が呼ばれる。

---

## bloadcast と received

```ruby:app/channels/chat_room.rb
class ChatChannel < ApplicationCable::Channel
  //...
  def foo(data)
    ActionCable.server.broadcast "ChatChannel", data
  end
end
```

`ChatChannel` の購読者全員に data をプッシュする

```js
cable.subscriptions.create("ChatChannel", {
  // ...
  received(data) {
    console.log(data) // => {data: 1, action: "foo"}
  }
});
```

`bloadcast_to` でユーザー個別に送ることもできる

---

## デモ

![](https://i.gyazo.com/f828c621f2568ede382fbd7057f6ade8.gif)


----

## **本当は**作りたかったもの

- 編集が1s止まったら入力を送信(ここまで実装済み)
- 3 way merge
    - 他人のチャンク編集後、他人のチャンク編集前、自分の状態
- 更新後のカーソル位置補正

----

## ActionCable の学習資料

- [Action Cable Overview — Ruby on Rails Guides](http://edgeguides.rubyonrails.org/action_cable_overview.html "Action Cable Overview — Ruby on Rails Guides")
- [rails/actioncable-examples: Action Cable Examples](https://github.com/rails/actioncable-examples "rails/actioncable-examples: Action Cable Examples")

----

## 閑話休題: Elixir/Phoenix

- Phoenix.Channel は ActionCable 丸パクリ(作者はRailsコミッタの Chris McCord)
- プロセスモデル/応答性の観点からはPhoenixの方が WebSocket に向いてそう

[翻訳: 似て非なる Phoenix と Rails（原題『Phoenix is not Rails』） - Qiita](http://qiita.com/mserizawa/items/f32469bcac61aa6fa491 "翻訳: 似て非なる 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 に保存する

- ここまで来ると、あとは [オンラインゲームを支える技術](https://www.amazon.co.jp/dp/4774145807/ref=cm_sw_r_tw_dp_x_zxeTxbTC39VSA) などが参考になる

---

## 趣味でオンラインゲーム作ってた時

- ダンジョン階層ごとにチャンネル分割
- 12FPSでゲームステートを配信
- クライアントは60FPSなので、時系列データから他のEntityの座標予測
- モンスターを倒したら保存

---

## 最後に

- まだ負荷計測に至ってないので、どこがボトルネックになるかわかってない
- 高頻度イベントはWebのお約束が通じなかったりするんで、頑張りましょう

---

## 終わり