Action CableのREADMEを読んでみた! 

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


🎳 Action Cableが登場した背景

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

🍣 Action Cable - README

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

🍮 用語

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

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

😎 (例)

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

# 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 を継承したクラスを作成。

# app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end

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

# app/assets/javascripts/cable.coffee
#= require action_cable
@App = {}
App.cable = ActionCable.createConsumer("ws://cable.example.com")

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

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

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

# 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などを使うことができる。

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

# 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名を変更。

# app/channels/web_notifications_channel.rb
class WebNotificationsChannel < ApplicationCable::Channel
def subscribed
stream_from "web_notifications_#{current_user.id}"
end
end

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

# 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(配信)する。

# 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 (設定)

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

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

Subscription adapterの設定(Redis)

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

production: &production
adapter: redis
url: redis://10.10.3.153:6381
development: &development
adapter: redis
url: redis://localhost:6379
test: *development

特定のドメインのみ許可

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

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

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

Consumerの設定

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

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

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

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

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

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

とする。

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

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

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

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

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

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

<%= action_cable_meta_tag %>

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

App.cable = ActionCable.createConsumer()

注意点

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

😀 ActionCalbeのサーバを動かす

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

通常のアプリケーションサーバとAction Cable用のサーバを分けて動かす場合は、Rack Applicationとして動かす。

# cable/config.ru
require ::File.expand_path('../../config/environment', __FILE__)
Rails.application.eager_load!
run ActionCable.server

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

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

アプリケーション内で動かす場合(In App)

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

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

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

🗽 注意点

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

🐠 依存関係

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

🎂 デプロイ

Action CableはWebsocketとスレッドによって作られている。

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

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

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

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

😼 パフォーマンス

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

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

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

Action Cable + Pumaをクラスタモードにして4ワーカ プロセスにした場合;

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

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

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

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

🏀 メモ

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

👽 参考リンク

📚 おすすめの書籍