Action Cable の概要

本ガイドでは、Action Cableのしくみと、WebSocketをRailsアプリケーションに導入してリアルタイム機能を実現する方法について解説します。

このガイドの内容:

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 チャネル

チャネル (Channel) は、論理的な作業単位をカプセル化します。通常のMVC設定でコントローラが果たす役割と似ています。Railsはデフォルトで、チャネル間で共有されるロジックをカプセル化するApplicationCable::Channelという親クラスを作成します。

3.2.1 親チャネルの設定
# app/channels/application_cable/channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end

上のコードによって、専用のChannelクラスを作成します。たとえば、 ChatChannelAppearanceChannelなどは次のように作成します。

# 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 ストリーム

ストリーム(stream)は、ブロードキャストでパブリッシュするコンテンツをサブスクライバ側にルーティングする機能をチャネルに提供します。

# 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つの例で共通です。

  1. コネクションを設定
  2. [親チャネルを設定](#親チャネルの設定
  3. コンシューマーを接続

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 クライアント-サーバー間のやりとり
  1. クライアントサーバーApp.cable = ActionCable.createConsumer("ws://cable.example.com")経由で接続する(cable.js)。サーバーは、このコネクションの認識にcurrent_userを使う。

  2. クライアントはアピアランスチャネルにApp.cable.subscriptions.create(channel: "AppearanceChannel")経由で接続する(appearance.coffee

  3. サーバーは、アピアランスチャネル向けに新しいサブスクリプションを開始したことを認識し、サーバーのsubscribedコールバックを呼び出し、current_userappearメソッドを呼び出す。(appearance_channel.rb

  4. クライアントは、サブスクリプションが確立したことを認識し、connectedappearance.coffee)を呼び出す。これにより、@install@appearが呼び出される。@appearはサーバーのAppearanceChannel#appear(data)を呼び出して{ appearing_on: $("main").data("appearing-on") }のデータハッシュを渡す。なお、この動作が可能なのは、クラスで宣言されている(コールバックを除く)全パブリックメソッドが、サーバー側のチャネルインスタンスから自動的に公開されるからです。公開されたパブリックメソッドは、サブスクリプションでperformメソッドを使って、RPC(リモートプロシージャコール)として利用できます。

  5. サーバーは、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.ymlpool属性で変更できます。

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-drivernio4rconcurrent-rubyの上に構築されています。

10 デプロイ

Action Cableを支えているのは、WebSocketとスレッドの組み合わせです。フレームワーク内部の流れや、ユーザー指定のチャネルの動作は、Rubyのネイティブスレッドによって処理されます。つまり、スレッドセーフを損なわない限り、Railsの正規のモデルはすべて問題なく利用できるということです。

Action Cableサーバーには、RackソケットをハイジャックするAPIが実装されています。これによって、アプリケーション・サーバーがマルチスレッドであるかどうかにかかわらず、内部のコネクションをマルチスレッドパターンで管理できます。

つまり、Action Cableは、Unicorn、Puma、Passengerなどの有名なサーバーと問題なく連携できるのです。