酒と泪とRubyとRailsと

Ruby on Rails と Objective-C は酒の肴です!

Action CableのREADMEを読んでみた!

Action Cable についてしらべてみたのでそのメモ。とは言っても公式のREADME 『rails/actioncable at master · rails/rails』 を主に読みました。READMEも内容が充実していて結構勉強になりました。 ActionCableもう少し入門したいなという人がいたら是非読んでみてください^^


ActionCableが登場した背景

Userの入力なしに、最新の情報をWebに表示する「Realtime性」を持つリッチな 体験をユーザーに提供したいというニーズが増えてきている。こういったにニーズへの対応。

ActionCable - README

ActionCableとは、『RailsのRESTとWebSocketのシームレスな統合』である。

用語

用語 Consumer, Channel, Subscribe についての説明。

- 1つのActionCableのサーバが、複数のコネクション(Consumer)をハンドリングする
- 一人のユーザーが複数のChannelをSubscribeすることがある
- Channelを購読している人に対して ストリーミング or ブロードキャストする

(例)

接続してきたユーザーが許可するべきユーザーかを判定して、接続を確立する。 コネクションの再確立等のために、ユーザーごとに indetify を行う。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected

    def find_verified_user
      if current_user = User.find_by(id: cookies.signed[:user_id])
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

ApplicationCable::Channel を継承したクラスを作成。

1
2
3
4
5
# app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

consumer 側にもコネクションのインスタンスを確立するための設定を書く。

1
2
3
4
5
# app/assets/javascripts/cable.coffee
#= require action_cable

@App = {}
App.cable = ActionCable.createConsumer("ws://cable.example.com")

ws://cable.example.com は ActionCable用のサーバ。CookieのネームスペースのためにREST アプリ(例: http://example.com)の配下にすること。 そうすることで正しく、cookieを送信することができる。

(例1) ユーザーのONLINE/OFFLINE の表示

ユーザーがOnline/Offlineを判定する仕組み。まずはサーバーサイドのChannelについて。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
  def subscribed
    current_user.appear
  end

  def unsubscribed
    current_user.disappear
  end

  def appear(data)
    current_user.appear on: data['appearing_on']
  end

  def away
    current_user.away
  end
end

#subscribed が呼び出されるとクライアントサイドのサブスクリプションが立ち上がる。

appear/disappear のAPIには、RedisもしくはDBなどを使うことができる。

クライアントサイドのコードはこちら。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# app/assets/javascripts/cable/subscriptions/appearance.coffee
App.cable.subscriptions.create "AppearanceChannel",
  # Called when the subscription is ready for use on the server
  connected: ->
    @install()
    @appear()

  # Called when the WebSocket connection is closed
  disconnected: ->
    @uninstall()

  # Called when the subscription is rejected by the server
  rejected: ->
    @uninstall()

  appear: ->
    # Calls `AppearanceChannel#appear(data)` on the server
    @perform("appear", appearing_on: $("main").data("appearing-on"))

  away: ->
    # Calls `AppearanceChannel#away` on the server
    @perform("away")


  buttonSelector = "[data-behavior~=appear_away]"

  install: ->
    $(document).on "page:change.appearance", =>
      @appear()

    $(document).on "click.appearance", buttonSelector, =>
      @away()
      false

    $(buttonSelector).show()

  uninstall: ->
    $(document).off(".appearance")
    $(buttonSelector).hide()

これで ユーザーが Online/Offline になった時に表示を切り替えるといった処理を実装できる。

(例2) 個別のユーザーへのノーティフィケーション

Channelの定義はこちら。ユーザー単位にChannel名を変更。

1
2
3
4
5
6
# app/channels/web_notifications_channel.rb
class WebNotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_from "web_notifications_#{current_user.id}"
  end
end

ノーティフィケーションのタイトルと本文を受け取る部分。

1
2
3
4
# Client-side, which assumes you've already requested the right to send web notifications
App.cable.subscriptions.create "WebNotificationsChannel",
  received: (data) ->
    new Notification data["title"], body: data["body"]

コメント配信用のJobを作成してその中でNotificationをBroadcast(配信)する。

1
2
# Somewhere in your app this is called, perhaps from a NewCommentJob
ActionCable.server.broadcast "web_notifications_#{current_user.id}", { title: 'New things!', body: 'All the news that is fit to print' }
  • ActionCable.server.broadcast は Redis の pubsub キューに基づいてメッセージを配信する
  • 生きているchannelに対して配信され #received(data) がコールされる
  • サーバサードに JSONがサーブされて #receivedが受け取る

その他幾つかのサンプルがある。

rails/actioncable-examples: Action Cable Examples

に幾つかのサンプルがあるので、こちらを見てみるといいかも。

Configuration (設定)

ActionCable を使う上で必要となる次の3つの設定について。

- a subscription adapter
- allowed request origins request origins の許可
- ActionCable用のサーバーURL

Subscription adapterの設定(Redis)

ActionCable::Server::Base はデフォルトで Rails.root.join('config/cable.yml') を見ている。 この YAMLファイルは以下のように環境ごとの設定を書くことができる。

1
2
3
4
5
6
7
production: &production
  adapter: redis
  url: redis://10.10.3.153:6381
development: &development
  adapter: redis
  url: redis://localhost:6379
test: *development

特定のドメインのみ許可

ActionCableは許可された Origin からのリクエストしか受け付けない。受け付ける Origin は正規表現等で書くこともできる。

1
Rails.application.config.action_cable.allowed_request_origins = ['http://rubyonrails.com', /http:\/\/ruby.*/]

Development モードでは、http://localhost:3000 のみ許可。

Consumer の設定

Cableサーバーを決めたら、ServerのURLをclientサイドに提供する必要がある。

シンプルにConsumerの作成時に渡す場合

独立したサーバーの場合は;

1
App.cable = ActionCable.createConsumer("ws://example.com:28080")

Appサーバに含める場合は;

1
App.cable = ActionCable.createConsumer("/cable")

とする。

タグを介して設定ファイルから渡す場合

上記以外のパターンとして、View側に action_cable_meta_tag を書いて設定ファイルの config.action_cable.url の内容を渡すこともできる。 これは環境ごとにWebsocketのURLが変わる場合に有効な手法である。もし、https を使っている場合は wss schemaを使う必要がある。

ちなみに、 wswss とはWebsocketのスキームのことです。 wswss の違いは、wss がSSL通信を行うためのスキームという点です。

設定例としては、環境ごとに以下のような設定を書く。

1
config.action_cable.url = "ws://example.com:28080"

View側に JavaScript のタグとして以下のような記述を行う

1
<%= action_cable_meta_tag %>

コンシューマは次のように作成する。

1
App.cable = ActionCable.createConsumer()

注意点

workerの数だけ、DBへのコネクションが発生する点である。 database.yml に pool数があるので適切に設定すること。

ActionCalbeのサーバーを動かす

ActionCable専用のサーバを動かす(Standalone)

通常のアプリサーバとActionCable用のサーバを分けて動かす場合は、Rack Applicationとして動かす。

1
2
3
4
5
# cable/config.ru
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!

run ActionCable.server

そして binstubとして bin/cable を作る。

1
2
#!/bin/bash
bundle exec puma -p 28080 cable/config.ru

アプリ内で動かす場合(In App)

Puma や Thin のようなスレッドサーバを使っている場合は、アプリ内でActionCableの実装を動かす事もできる。 /cableのWebSocketのリクエストを受け取る場合の設定例がこちら。

1
2
3
4
# config/routes.rb
Example::Application.routes.draw do
  mount ActionCable.server => '/cable'
end

RedisがメッセージをSyncさせつつ、動いているよう。

注意点

現時点では、cableサーバに関する部分はauto-reloadされていない。なので、Channelや関連するモデルを変更した場合はActionCableのサーバを再起動する必要がある。

依存関係

ActionCableはpub/subの機能を持ったプロセスとのインターフェースのアダプタを持っている。PostgreSQLやRedisなど。

デプロイ

ActionCableはwebsocketとスレッドによって作られている。

サーバでは、通常のWebのプロセスとActionCableのプロセスが存在することになる。

ActionCableは、Rack socket hijacking API を使っている。

これによって、マルチスレッドでのconnectionの管理を実現させている。これにより、サーバがマルチスレッドでなくてもActionCableを使うことができる。 (Unicorn, Puma, Passenger で動かすことができる)

Rails5で WebrickからPumaに開発サーバが変わったのは、Webrickが Rack socket hijacking API をサポートしていなかったからである。

パフォーマンス

Action Cable - Friend or Foe?』の抜粋。

ActionCable + Puma (16スレッド)の場合;

WebSocketの同時接続数 平均接続時間
3 17ms
30 196ms
300 1638ms

ActionCable + Pumaをクラスタモードにして 4 worker プロセスにした場合;

WebSocketの同時接続数 平均接続時間
3 9ms
30 89ms
300 855ms

Node.js + Websocketのシンプルなチャットアプリ 『Action Cable - Friend or Foe?』 と比較した場合;

WebSocketの同時接続数 平均接続時間
3 5ms
30 65ms
300 3600ms

現実的な環境とは異なるため、一概には比較しきれないが、ActionCableでも十分なパフォーマンスを出せている事がわかる。

メモ

  • Polling => Good Solutionだけどスケーラビリティやモバイルでの用途に課題がある
  • Server-sent Events (SSEs) => Rails4で ActionController::Liveとして提供。永続的なコネクションを行うため、HerokuやWebrickなどで動作しなかった
  • Websocket => ステートを持った接続を行う。クライアントとサーバ間の接続の数だけコネクションを持っている。
  • Sticky Session => ActionCableはRedisのpub/sub機能を使うことで、複数スレッドでも安定してデータを配信することができる

Special Thanks

おすすめの書籍