1 はじめに
Action Cableは、 WebSocketとRailsのその他の部分をシームレスに統合するためのものです。Action Cable が導入されたことで、Rails アプリケーションの効率の良さとスケーラビリティを損なわずに、通常のRailsアプリケーションと同じスタイル・方法でリアルタイム機能をRubyで記述できます。クライアント側のJavaScriptフレームワークとサーバー側のRubyフレームワークを同時に提供する、フルスタックのフレームワークです。Active RecordなどのORMで書かれたすべてのドメインモデルにアクセスできます。
2 Pub/Subについて
Pub/Subはパブリッシャ-サブスクライバ(pub/sub)型モデルとも呼ばれる、メッセージキューのパラダイムです。パブリッシャ側(Publisher)が、サブスクライバ側(Subscriber)の抽象クラスに情報を送信します。 このとき、個別の受信者を指定しません。Action Cableは、サーバーと多数のクライアント間の通信にこのアプローチを採用しています。
3 サーバー側のコンポーネント
3.1 接続
接続(connection)は、クライアントとサーバー間の関係を成立させる基礎となります。サーバーでWebSocketを受け付けるたびに、接続オブジェクトがインスタンス化します。このオブジェクトは、今後作成されるすべてのチャネルサブスクライバの親となります。この接続自体は、認証や承認の後、特定のアプリケーションロジックを扱いません。WebSocket接続のクライアントはコンシューマーと呼ばれます。各ユーザーが開くブラウザタブ、ウィンドウ、デバイスごとに、コンシューマー接続ペアが1つずつ作成されます。
接続は、ApplicationCable::Connection
のインスタンスです。このクラスでは、着信接続を承認し、ユーザーを特定できた場合に接続を確立します。
3.1.1 接続の設定
# 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 private def find_verified_user if verified_user = User.find_by(id: cookies.encrypted[:user_id]) verified_user else reject_unauthorized_connection end end end end
上のidentified_by
は接続IDであり、後で特定の接続を見つけるときに利用できます。IDとしてマークされたものは、その接続以外で作成されるすべてのチャネルインスタンスに、同じ名前で自動的にデリゲートを作成します。
この例では、アプリケーションの他の場所で既にユーザー認証を扱っており、認証成功によってユーザーIDに署名済みcookieが設定されていることを前提としています。
次に、新しい接続を求められたときにこのcookieが接続インスタンスに自動で送信され、current_user
の設定に使われます。現在の同じユーザーによる接続が識別されると、そのユーザーが開いているすべての接続を取得することも、ユーザーが削除されたり認証できない場合に切断することもできるようになります。
3.2 チャネル
チャネルは、論理的な作業単位をカプセル化します。通常のMVC設定でコントローラが果たす役割と似ています。Railsはデフォルトで、チャネル間で共有されるロジックをカプセル化するApplicationCable::Channel
という親クラスを作成します。
3.2.1 親チャネルの設定
# app/channels/application_cable/channel.rb module ApplicationCable class Channel < ActionCable::Channel::Base end end
上のコードによって、専用のChannelクラスを作成します。たとえば、
ChatChannel
やAppearanceChannel
などは次のように作成します。
# app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel end # app/channels/appearance_channel.rb class AppearanceChannel < ApplicationCable::Channel end
これで、コンシューマーはこうしたチャネルをサブスクライブできるようになります。
3.2.2 サブスクリプション
コンシューマーは、チャネルを購読するするサブスクライバ側(Subscriber)の役割を果たします。そして、コンシューマーの接続はサブスクリプション(Subscription: 購読)と呼ばれます。生成されたメッセージは、Action Cableコンシューマーが送信するIDに基いて、これらのチャネルサブスクライバ側にルーティングされます。
# app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel # コンシューマーがこのチャネルのサブスクライバ側になると # このコードが呼び出される def subscribed end end
4 クライアント側のコンポーネント
4.1 接続
コンシューマー側でも、接続のインスタンスが必要になります。この接続は、Railsがデフォルトで生成する次のJavaScriptコードによって確立します。
4.1.1 コンシューマーとの接続
// app/assets/javascripts/cable.js //= require action_cable //= require_self //= require_tree ./channels (function() { this.App || (this.App = {}); App.cable = ActionCable.createConsumer(); }).call(this);
これにより、サーバーの/cable
にデフォルトで接続するコンシューマーが準備されます。利用したいサブスクリプションを1つ以上指定しなければ接続は確立しません。
4.1.2 サブスクライバ側
指定のチャネルにサブスクリプションを作成することで、コンシューマーがサブスクライバ側になります。
# app/assets/javascripts/cable/subscriptions/chat.coffee App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" } # app/assets/javascripts/cable/subscriptions/appearance.coffee App.cable.subscriptions.create { channel: "AppearanceChannel" }
サブスクリプションは上のコードで作成されます。受信したデータに応答する機能については後述します。
コンシューマーは、指定のチャネルに対するサブスクライバ側として振る舞うことができます。回数の制限はありません。たとえば、コンシューマーはチャットルームを同時にいくつでもサブスクライブできます。
App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" } App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
5 クライアント-サーバー間のやりとり
5.1 ストリーム
ストリームは、ブロードキャストでパブリッシュするコンテンツをサブスクライバ側にルーティングする機能をチャネルに提供します。
# app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed stream_from "chat_#{params[:room]}" end end
あるモデルに関連するストリームを作成すると、利用するブロードキャストがそのモデルとチャネルから生成されます。次の例 では、comments:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE
のような形式のブロードキャストでサブスクライブします。
class CommentsChannel < ApplicationCable::Channel def subscribed post = Post.find(params[:id]) stream_for post end end
これで、このチャネルに次のようにブロードキャストできるようになります。
CommentsChannel.broadcast_to(@post, @comment)
5.2 ブロードキャスト
ブロードキャスト(broadcasting)は、pub/subのリンクです。パブリッシャ側からの送信内容はすべてブロードキャストを経由し、その名前のブロードキャストをストリーミングするチャネルサブスクライバ側に直接ルーティングされます。各チャネルは、0個以上のブロードキャストをストリーミングできます。
ブロードキャストは純粋なオンラインキューであり、時間に依存します。ストリーミング(指定のチャネルへのサブスクライバ)を行っていないコンシューマーは、後で接続するときにブロードキャストを取得できません。
ブロードキャストは、Railsアプリケーションの別の場所で呼び出されます。
WebNotificationsChannel.broadcast_to( current_user, title: 'New things!', body: 'All the news fit to print' )
WebNotificationsChannel.broadcast_to
呼び出しでは、メッセージを現在のサブスクリプションアダプタ(production環境のデフォルトはRedis、development/test環境のデフォルトはasync
)のpubsubキューに設定します。ブロードキャスト名はユーザーごとに異なります。IDが1のユーザーなら、ブロードキャスト名はweb_notifications:1
のようになります。
このチャネルは、web_notifications:1
に着信するものすべてをreceived
コールバック呼び出しによってクライアントに直接ストリーミングするようになります。
5.3 サブスクリプション
チャネルをサブスクライブしたコンシューマーは、サブスクライバ側として振る舞います。この接続もサブスクリプション (Subscription: サブスクライバ) と呼ばれます。着信メッセージは、Action Cableコンシューマーが送信するIDに基いて、これらのチャネルサブスクライバ側にルーティングされます。
# app/assets/javascripts/cable/subscriptions/chat.coffee # web通知の送信権をサーバーからリクエスト済みであることが前提 App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, received: (data) -> @appendLine(data) appendLine: (data) -> html = @createLine(data) $("[data-chat-room='Best Room']").append(html) createLine: (data) -> """ <article class="chat-line"> <span class="speaker">#{data["sent_by"]}</span> <span class="body">#{data["body"]}</span> </article> """
5.4 チャネルにパラメータを渡す
サブスクリプション作成時に、クライアント側のパラメータをサーバー側に渡すことができます。以下に例を示します。
# app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed stream_from "chat_#{params[:room]}" end end
subscriptions.create
に最初の引数として渡されるオブジェクトは、Action Cableチャネルのparamsハッシュになります。キーワードchannel
の指定は省略できません。
# app/assets/javascripts/cable/subscriptions/chat.coffee App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, received: (data) -> @appendLine(data) appendLine: (data) -> html = @createLine(data) $("[data-chat-room='Best Room']").append(html) createLine: (data) -> """ <article class="chat-line"> <span class="speaker">#{data["sent_by"]}</span> <span class="body">#{data["body"]}</span> </article> """
# このコードはアプリのどこかで呼び出される # おそらくNewCommentJobなどのあたりで ActionCable.server.broadcast( "chat_#{room}", sent_by: 'Paul', body: 'This is a cool chat app. ' )
5.5 メッセージを再ブロードキャストする
あるクライアントから、接続している別のクライアントに、メッセージを再ブロードキャストすることはよくあります。
# app/channels/chat_channel.rb class ChatChannel < ApplicationCable::Channel def subscribed stream_from "chat_#{params[:room]}" end def receive(data) ActionCable.server.broadcast("chat_#{params[:room]}", data) end end
# app/assets/javascripts/cable/subscriptions/chat.coffee App.chatChannel = App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" }, received: (data) -> # data => { sent_by: "Paul", body: "This is a cool chat app." } App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
再ブロードキャストは、接続しているすべてのクライアントで受信されます。送信元クライアント自身も再ブロードキャストを受信します。利用するparamsは、チャネルをサブスクライブするときと同じです。
6 フルスタックの例
以下の設定手順は、2つの例で共通です。
- 接続を設定
- [親チャネルを設定](#親チャネルの設定
- コンシューマーを接続
6.1 例1: ユーザーアピアランスの表示
これは、ユーザーがオンラインかどうか、ユーザーがどのページを開いているかという情報を追跡するチャネルの簡単な例です(オンラインユーザーの横に緑の点を表示する機能を作成する場合などに便利です)。
サーバー側のアピアランスチャネルを作成します。
# 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
コールバックがトリガーされ、そのユーザーがオンラインであることが示されます。このアピアランスAPIをRedisやデータベースなどと連携することもできます。
クライアント側のアピアランスチャネルを作成します。
# app/assets/javascripts/cable/subscriptions/appearance.coffee App.cable.subscriptions.create "AppearanceChannel", # サブスクリプションがサーバー側で利用可能になると呼び出される connected: -> @install() @appear() # WebSocket接続が閉じると呼び出される disconnected: -> @uninstall() # サブスクリプションがサーバーに拒否されると呼び出される rejected: -> @uninstall() appear: -> # サーバーの`AppearanceChannel#appear(data)`を呼び出す @perform("appear", appearing_on: $("main").data("appearing-on")) away: -> # サーバーの`AppearanceChannel#away`を呼び出す @perform("away") buttonSelector = "[data-behavior~=appear_away]" install: -> $(document).on "turbolinks:load.appearance", => @appear() $(document).on "click.appearance", buttonSelector, => @away() false $(buttonSelector).show() uninstall: -> $(document).off(".appearance") $(buttonSelector).hide()
6.1.1 クライアント-サーバー間のやりとり
クライアントはサーバーに
App.cable = ActionCable.createConsumer("ws://cable.example.com")
経由で接続する(cable.js
)。サーバーは、この接続の認識にcurrent_user
を使う。クライアントはアピアランスチャネルに
App.cable.subscriptions.create(channel: "AppearanceChannel")
経由で接続する(appearance.coffee
)サーバーは、アピアランスチャネル向けに新しいサブスクリプションを開始したことを認識し、サーバーの
subscribed
コールバックを呼び出し、current_user
のappear
メソッドを呼び出す。(appearance_channel.rb
)クライアントは、サブスクリプションが確立したことを認識し、
connected
(appearance.coffee
)を呼び出す。これにより、@install
と@appear
が呼び出される。@appear
はサーバーのAppearanceChannel#appear(data)
を呼び出して{ appearing_on: $("main").data("appearing-on") }
のデータハッシュを渡す。なお、この動作が可能なのは、クラスで宣言されている(コールバックを除く)全パブリックメソッドが、サーバー側のチャネルインスタンスから自動的に公開されるからです。公開されたパブリックメソッドは、サブスクリプションでperform
メソッドを使って、RPC(リモートプロシージャコール)として利用できます。サーバーは、
current_user
で認識した接続のアピアランスチャネルで、appear
アクションへのリクエストを受信する。(appearance_channel.rb
)サーバーは:appearing_on
キーを使ってデータをデータハッシュから取り出し、current_user.appear
に渡される:on
キーの値として設定する。
6.2 例2: 新しいweb通知を受信する
この例では、WebSocket接続を使って、サーバーからクライアント側の機能をリモート実行するときのアピアランスを扱います。WebSocketでは双方向通信を利用できます。そこで、例としてサーバーからクライアントでアクションを起動してみます。
このweb通知チャネルは、正しいストリームにブロードキャストを行ったときに、クライアント側でweb通知を表示します。
サーバー側のweb通知チャネルを作成します。
# app/channels/web_notifications_channel.rb class WebNotificationsChannel < ApplicationCable::Channel def subscribed stream_for current_user end end
クライアント側のweb通知チャネルを作成します。
# app/assets/javascripts/cable/subscriptions/web_notifications.coffee # クライアント側では、サーバーからweb通知の送信権を # リクエスト済みであることが前提 App.cable.subscriptions.create "WebNotificationsChannel", received: (data) -> new Notification data["title"], body: data["body"]
アプリケーションのどこからでも、web通知チャネルのインスタンスにコンテンツをブロードキャストできます。
# このコードはアプリのどこか(NewCommentJob あたり)で呼び出される WebNotificationsChannel.broadcast_to( current_user, title: 'New things!', body: 'All the news fit to print' )
WebNotificationsChannel.broadcast_to
呼び出しでは、現在のサブスクリプションアダプタのpubsubキューにメッセージを設定します。ユーザーごとに異なるブロードキャスト名が使用されます。IDが1のユーザーなら、ブロードキャスト名はweb_notifications:1
のようになります。
このチャネルは、web_notifications:1
に着信するものすべてをreceived
コールバック呼び出しによってクライアントに直接ストリーミングするようになります。引数として渡されたデータは、サーバー側のブロードキャスト呼び出しに2番目のパラメータとして渡されたハッシュです。このハッシュはJSONでエンコードされ、received
で受信したときにデータ引数から取り出されます。
6.3 より詳しい例
RailsアプリにAction Cableを設定する方法やチャネルの追加方法については、rails/actioncable-examples で完全な例をご覧いただけます。
7 設定
Action Cableで必須となる設定は、「サブスクリプションアダプタ」と「許可されたリクエスト送信元」の2つです。
7.1 サブスクリプションアダプタ
Action Cableは、デフォルトでconfig/cable.yml
の設定ファイルを利用します。Railsの環境ごとに、アダプタとURLを1つずつ指定する必要があります。アダプタについて詳しくは、依存関係 の節をご覧ください。
development: adapter: async test: adapter: async production: adapter: redis url: redis://10.10.3.153:6381 channel_prefix: appname_production
7.1.1 利用できるアダプタ設定
以下は、エンドユーザー向けに利用できるサブスクリプションアダプタの一覧です。
7.1.1.1 Asyncアダプタ
async
アダプタはdevelopment環境やtest環境での利用を意図したものであり、production環境で使うべきではありません。
7.1.1.2 Redisアダプタ
Redisアダプタでは、Redisサーバーを指すURLを指定する必要があります。
また、複数のアプリが同一のRedisサーバーを用いる場合は、チャンネル名衝突を避けるためにchannel_prefix
の指定が必要になることもあります。詳しくはRedis PubSubドキュメントを参照してください。
7.1.1.3 PostgreSQLアダプタ
PostgreSQLアダプタはActive Recordコネクションプールを用いるため、アプリのconfig/database.yml
データベース設定を接続に使います。将来変更される可能性があります。#27214
7.2 許可されたリクエスト送信元
Action Cableは、指定されていない送信元からのリクエストを受け付けません。送信元リストは、配列の形でサーバー設定に渡します。送信元リストには文字列のインスタンスや正規表現を利用でき、これに対して一致するかどうかがチェックされます。
config.action_cable.allowed_request_origins = ['http://rubyonrails.com', %r{http://ruby.*}]
すべての送信元からのリクエストを許可または拒否するには、次を設定します。
config.action_cable.disable_request_forgery_protection = true
development環境で実行中、Action Cableはlocalhost:3000からのすべてのリクエストをデフォルトで許可します。
7.3 コンシューマーの設定
URLを設定するには、HTMLレイアウトのHEADセクションにaction_cable_meta_tag
呼び出しを追加します。通常、ここで使うURLは、環境ごとの設定ファイルでconfig.action_cable.url
に設定されます。
7.4 その他の設定
他にも、接続ごとのロガーにタグを保存するオプションがあります。次の例は、ユーザーアカウントIDがある場合はそれを使い、ない場合は「no-account」を使うタグ付けです。
config.action_cable.log_tags = [ -> request { request.env['user_account_id'] || "no-account" }, :action_cable, -> request { request.uuid } ]
利用可能なすべての設定オプションについては、ActionCable::Server::Configuration
クラスをご覧ください。
もう1つ注意が必要な点があります。サーバーが提供するデータベース接続の数は、少なくともワーカー数を下回らないようにする必要があります。デフォルトのワーカープールサイズは4なので、データベース接続も4つは用意する必要があります。この値は、config/database.yml
のpool
属性で変更できます。
8 Action Cable専用サーバーを実行する
8.1 アプリで実行
Action CableはRailsアプリケーションと一緒に実行できます。たとえば、/websocket
でWebSocketリクエストをリッスンするには、config.action_cable.mount_path
でパスを指定します。
# config/application.rb class Application < Rails::Application config.action_cable.mount_path = '/websocket' end
レイアウトでaction_cable_meta_tag
を呼び出すと、App.cable = ActionCable.createConsumer()
でAction Cableサーバーに接続できるようになります。createConsumer
の最初の引数にはカスタムパスが指定されます(例: App.cable = ActionCable.createConsumer("/websocket")
)。
作成したサーバーの全インスタンスと、サーバーが作成した全ワーカーのインスタンスには、Action Cableの新しいインスタンスも含まれます。接続間のメッセージ同期は、Redisによって行われます。
8.2 スタンドアロン
アプリケーション・サーバーとAction Cableサーバーを分けることもできます。Action CableサーバーはRackアプリケーションですが、独自のRackアプリケーションでもあります。推奨される基本設定は次のとおりです。
# cable/config.ru require_relative '../config/environment' Rails.application.eager_load! run ActionCable.server
続いて、 bin/cable
のbinstubを使ってサーバーを起動します。
#!/bin/bash bundle exec puma -p 28080 cable/config.ru
ポート28080でAction Cableサーバーが起動します。
8.3 メモ
WebSocketサーバーからはセッションにアクセスできませんが、cookieにはアクセスできます。これを利用して認証を処理できます。Action CableとDeviseでの認証 記事をご覧ください。
9 依存関係
Action Cableは、自身のpubsub内部のプロセスへのサブスクリプションアダプタインターフェイスを提供します。非同期、インライン、PostgreSQL、Redisなどのアダプタをデフォルトで利用できます。新規Railsアプリケーションのデフォルトアダプタは非同期(async
)アダプタです。
Ruby側では、websocket-driver、 nio4r、concurrent-rubyの上に構築されています。
10 デプロイ
Action Cableを支えているのは、WebSocketとスレッドの組み合わせです。フレームワーク内部の流れや、ユーザー指定のチャネルの動作は、Rubyのネイティブスレッドによって処理されます。つまり、スレッドセーフを損なわない限り、Railsの正規のモデルはすべて問題なく利用できるということです。
Action Cableサーバーには、RackソケットをハイジャックするAPIが実装されています。これによって、アプリケーション・サーバーがマルチスレッドであるかどうかにかかわらず、内部の接続をマルチスレッドパターンで管理できます。
つまり、Action Cableは、Unicorn、Puma、Passengerなどの有名なサーバーと問題なく連携できるのです。