📕

Action Cableについて

2025/03/08に公開

開発環境

  • macOS
  • VSCode
  • Rails 7.1.3.3
  • ruby-3.2.3

行いたいこと

  • RailsガイドのAction Cableについて、自分なりにわかりやすいように噛み砕いたので覚書として記載します。

https://railsguides.jp/action_cable_overview.html




Action Cableとは

  • Action Cableは、フロントエンドのWebSocketバックエンドのRails環境をシームレスに統合する、フルスタックなフレームワークです。

  • つまり、フロントエンドバックエンドの間でスムーズな通信を可能にし、開発者がRails環境で効率的にリアルタイム機能を構築できるようにします。




WebSocketとは

  • WebSocketとは、Webアプリケーションにおいてサーバークライアント間で双方向のリアルタイム通信を実現するためのプロトコルです。

  • 従来のHTTPプロトコルでは、クライアントからのリクエストに対してサーバーが応答する一方向の通信モデルでしたが、WebSocketではコネクションを確立した後は、サーバー側からもクライアントにデータを送信できる双方向の通信が可能になります。

  • つまり、Action Cableでは、通常のHTTPリクエスト・レスポンスプロトコルの代わりにWebSocketを利用します。




コネクションとは

  • コネクション(connection)とは、クライアントとサーバーを繋ぐ通信経路のことです。

  • Action Cableサーバーは、複数のWebSocketコネクションを同時に扱うことができ、それぞれに対応するコネクションインスタンスが生成されます。

  • 例えば、同じユーザーが複数のブラウザタブを開いたり、異なるデバイスを使用した場合、それぞれ独立したWebSocketコネクションが確立され、アプリケーションと接続されます。




WebSocketコネクションとは

  • WebSocketコネクションとは、クライアントとサーバーの間で双方向のリアルタイム通信を可能にする通信方式のことです。

  • 通常のHTTP通信では、クライアントがリクエストを送るたびにサーバーがレスポンスを返しますが、WebSocketでは一度接続が確立されると、サーバーとクライアントの間で常にデータをやり取りできる状態が維持されます。




コンシューマーとは

  • WebSocketのクライアントコンシューマー(consumer)と呼ばれます。

  • Action Cableでは、クライアント側のJavaScriptフレームワークを使って、このコンシューマーを作成します。

  • ここで言う「コンシューマーを作成します」 とは、WebSocket接続を確立し、サーバーとリアルタイム通信を行うための仕組みを準備することを指します。




チャネルとは

  • チャネル(channelとは、それぞれ特定の機能を担当する通信の単位で、MVCのコントローラーのようにデータのやり取りや処理を行います。

  • コンシューマー(WebSocketのクライアント)は必ず1つ以上のチャネルサブスクライブする必要があり、複数のチャネルを同時にサブスクライブすることもできます。

チャネルの例

ChatChannel → チャットのメッセージを送受信する。
AppearancesChannel → ユーザーのオンライン状態を管理する。
1つのコンシューマーは、これらのチャネルのどちらか一方、または両方をサブスクライブできます。




サブスクライバとは

  • サブスクライバ(subscriber)とは、WebSocketなどのリアルタイム通信システムにおいて、特定のチャネルをサブスクライブしているコンシューマーのことです。

  • コンシューマーがチャネルをサブスクライブすると、そのコンシューマーはそのチャネルのサブスクライバになります。

  • サブスクライバとチャネルの間の接続は「サブスクリプション」と呼ばれます。一人のコンシューマーは同じチャネルに複数回サブスクライブすることも可能で、例えば複数のチャットルームを同時にサブスクライブできます。




Pub/Subとは

  • Pub/Subとは、
    情報を送る側(Publisher/パブリッシャ)が「誰に送るか」を直接指定せず、代わりに「どんな種類の情報か」という分類でメッセージを発信するシステムです。
    情報を受け取る側(Subscriber/サブスクライバー)は、自分が興味のある情報の種類を登録しておき、関連するメッセージが発信されたときだけ通知を受け取ります。

  • このPub/Subは、メッセージキューパラダイムの一種で、メッセージの非同期処理と一時保管の機能を提供します。

  • 送信者は「チャネル」や「トピック」といった抽象化された宛先(抽象クラス)にメッセージを送信します。

  • Pub/Subの主な利点は、送信者と受信者の疎結合を実現することです。

  • 送信者はどの受信者がメッセージを受け取るかを知る必要がなく、受信者も誰がメッセージを送信したかを必ずしも知る必要がありません。これにより、多対多の通信が効率化されます。




ブロードキャストとは

  • ブロードキャスト(broadcasting)とは、情報の送信者(ブロードキャスター/broadcaster)によって転送されるあらゆる情報を、チャネルの受信者(サブスクライバ)に直接送信するためのpub/subリンクのことを指します。

  • Pub/Subシステムでは、メッセージを受け取るかどうかを受信者が決めるため、ブロードキャスターは特定の受信者を指定せずにメッセージを送信します。ブロードキャストは、そのメッセージを購読している全ての受信者に一斉に送信する機能です。

  • サブスクライバは、その名前を持つブロードキャストの情報を継続的に受信(ストリーミング)します。また、各チャネルは、0個以上のブロードキャストを同時にストリーミングすることができます。




サーバー側のコンポネート(Rails側)

コネクション(Connection)

[コネクションの基本的な仕組み]
  • サーバーがWebSocketを1個受信するたびに、コネクションオブジェクトインスタンスが生成されます。
  • このオブジェクトは、後に作成されるすべてのチャネルサブスクリプション親オブジェクトとなります。
  • ユーザーの認証とアクセスの認可が完了した後、コネクションオブジェクト自体はアプリケーションロジックを処理しません。
  • WebSocketコネクションのクライアントコンシューマー(Consumer)と呼ばれます。
  • ユーザーが新しい「ブラウザタブ」「ウィンドウ」「デバイス」で接続するたびに、コンシューマコネクションが1個ずつ作成されます。
[コネクションの技術的な構成]
  • コネクションはApplicationCable::Connectionのインスタンスです。
  • ApplicationCable::ConnectionActionCable::Connection::Baseを継承しています。
  • ApplicationCable::Connectionでは、ユーザーを識別できる場合に限り、認証後に接続を確立します。
[コネクションの設定]
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
コードの解説
app/channels/application_cable/connection.rb
module ApplicationCable


 # 技術的な構成: コネクションは`ApplicationCable::Connection`のインスタンスである
 # 技術的な構成: `ApplicationCable::Connection`は`ActionCable::Connection::Base`を継承している
 class Connection < ActionCable::Connection::Base


   # 基本的な仕組み: このオブジェクトは後に作成されるすべてのチャネルサブスクリプションの親オブジェクトとなる
   # ユーザー識別のための設定
   identified_by :current_user


   # 基本的な仕組み: サーバーがWebSocketを1個受信するたびに接続が確立される
   # 技術的な構成: ユーザーを識別できる場合に限り、認証後に接続を確立する
   def connect
     self.current_user = find_verified_user
   end

   private


     # 基本的な仕組み: 認証と認可の処理を行う部分
     def find_verified_user


       # cookieからユーザーIDを取得して認証する
       if verified_user = User.find_by(id: cookies.encrypted[:user_id])
         verified_user
       else


         # 認証失敗時は接続を拒否する
         reject_unauthorized_connection
       end
     end
 end
end
[例外ハンドリングについて]
[例外ハンドリング]
  • 例としてrescue_fromを使う方法
app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    rescue_from StandardError, with: :report_error

    private
      def report_error(e)
        SomeExternalBugtrackingService.notify(e)
      end
  end
end


  • コードの解説
app/channels/application_cable/connection.rb
# デフォルトでは、捕捉されなかった例外は Rails のログに出力される。
# しかし、これらの例外をグローバルに処理し、外部のバグトラッキングサービスに通知することも可能。
# その場合、rescue_from を使用してエラーハンドリングを行う。

module ApplicationCable
  class Connection < ActionCable::Connection::Base


    # StandardError(標準的なエラー)を捕捉し、report_error メソッドで処理する
    rescue_from StandardError, with: :report_error

    private


      # エラーが発生した際に、外部のバグトラッキングサービスに通知を送る
      def report_error(e)
        SomeExternalBugtrackingService.notify(e) # 例: `Sentry`や`Bugsnag`などのサービスを利用
      end
  end
end


ApplicationCable::Connectionクラスの解説
・このコードはRailsAction Cableにおける接続クラスの定義です。
ActionCable::Connection::Baseを継承してWebSocket接続を管理します。
・主な機能は例外処理(エラーハンドリング)の設定です。

例外処理機能
rescue_fromメソッドを使用して例外を捕捉しています。
StandardError(ほとんどの一般的なエラー)が対象です。
・エラー発生時はreport_error メソッドが呼び出されます。

report_errorメソッド
・捕捉したエラーを外部サービスに通知する役割があります。
・引数 e として捕捉された例外オブジェクトを受け取ります。
SomeExternalBugtrackingService.notify(e) でエラー情報を送信します。
・実際の実装ではSentryBugsnagなどの実際のサービス名が使われます。

設計上のポイント
プライベートメソッドとして定義されているため、外部からは直接呼び出せません。
・Action Cable 全体でのグローバルなエラーハンドリングを実現しています。
・エラーを記録するだけでなく、外部サービスへの通知も行います。


チャネル(Channel)

[チャネルとは]
  • チャネル(Channel)は、特定の機能や処理をまとめる単位であり、リアルタイム通信の処理を行う場所です。

  • MVC設計パターンの「コントローラ」に似た役割を果たし、クライアントとサーバー間でやり取りするデータを管理します。

  • Action Cableのコア機能の一つであり、WebSocket接続内で特定の処理を行うために使用されます。

[ApplicationCable::Channel について]
  • チャネルジェネレータを初めて使うと自動的に作成されます。(プロジェクトで最初にrails generate channelを実行したときに作成される)

  • ApplicationCable::ChannelActionCable::Channel::Baseを継承し、各チャネルで共通する処理を定義できる親クラスです。

  • 複数のチャネル間で共有されるロジックをカプセル化する親クラスです。

[システム構造における位置づけ]
  • チャネルは、WebSocketを通じてリアルタイム通信を処理するためのロジックを実装する場所です。
  • 複数のチャネルで共通の機能は親クラス(ApplicationCable::Channel)に配置し、各チャネルはそれを継承することで共通の処理を利用できます。
  • MVCの考え方をリアルタイム通信にも適用し、クライアントとサーバー間のイベント管理を分離しやすくする構造になっています。
[親チャネルと個別チャネルの設計]
  • 以下の記載でクライアント側からコンシューマーがチャネルをサブスクライブできるようになります。(ブラウザなどがWebSocketを通じて特定のチャネルに接続し、リアルタイム通信を利用できるようになります)

ApplicationCable::Channel すべてのチャネルの親クラス(親チャネルの設定)

app/channels/application_cable/channel.rb
module ApplicationCable
 # 役割: 共通機能を提供し、すべての独自チャネルがこれを継承する
 # ActionCable::Channel::Baseを継承して基本的なチャネル機能を取得
 class Channel < ActionCable::Channel::Base
 end
end

ChatChannel - チャット機能専用のチャネル

app/channels/chat_channel.rb
# 役割: チャットメッセージの送受信、プレゼンス管理などの
# チャット関連のビジネスロジックを実装する
# ApplicationCable::Channelを継承して共通機能を取得
class ChatChannel < ApplicationCable::Channel
end

AppearanceChannel - ユーザーのオンライン状態を管理するチャネル

app/channels/appearance_channel.rb
# 役割: ユーザーのログイン状態、オンライン/オフライン状態の変更通知、
# アクティビティステータスなどを管理
# ApplicationCable::Channelを継承して共通機能を取得
class AppearanceChannel < ApplicationCable::Channel
end
[サブスクリプション(Subscription)]
  • コンシューマー(Consumer)はチャネルをサブスクライブ(Subscribe)し、サブスクライバ(Subscriber)の役割を果たす。
  • コンシューマーのコネクションサブスクリプション(Subscription: 購読)と呼ばれる。
  • 生成されたメッセージは、Action Cableのコンシューマー(クライアント)が送信するidをもとに適切な宛先へ送られます。
app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
  # コンシューマーがこのチャネルのサブスクライバになると
  # このコードが呼び出される
  def subscribed
  end
end
subscribedメソッドへの記載について
  • subscribedメソッドの中身は空のため、メッセージの受信機能は未実装。
  • stream_fromを追加することで、特定のチャンネルからメッセージを受信できるようになる。
  • stream_from "chat_channel"を使うと、すべてのサブスクライバが同じチャットストリームを受信する。
  • stream_from "chat_channel_#{params[:room]}" を使うと、チャットルームごとに異なるストリームを受信できる。
[例外ハンドリングについて]
[例外ハンドリング]
  • ApplicationCable::Connectionの場合と同様、rescue_fromを利用すると特定チャネルで発生する例外を扱えるようになります。
app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel

  # "MyError" というカスタム例外が発生した場合に `deliver_error_message` メソッドを呼び出す
  rescue_from "MyError", with: :deliver_error_message

  private

    # 例外発生時にエラーメッセージをクライアントに送信するメソッド
    def deliver_error_message(e)
      # ここでエラーメッセージをブロードキャストする処理を実装する
      # 例: broadcast_to(user, { error: e.message })
      # broadcast_to(...)
    end
end
[チャネルの4つの主要なコールバックについて]
  • ActionCable::Channel::Callbacksは、チャネルのライフサイクルの間に呼び出される以下のコールバックフックを提供します。

[4つの主要なコールバック]


before_subscribe(購読前)

・コンシューマー(クライアント)がチャネルに サブスクライブする前 に実行される処理。

[例: サブスクライブを許可するかどうかのチェックする。]

class ChatChannel < ApplicationCable::Channel
  before_unsubscribe :save_unread_messages

  private

  # 権限がないユーザーはチャットにサブスクライブできなくなる
  def save_unread_messages
    current_user.update(last_seen_at: Time.current)
  end
end


after_subscribe(エイリアス: on_subscribe)(購読後)

・クライアントがサブスクライブした後 に実行される処理。

[例: ユーザーをオンラインリストに追加する。]

class ChatChannel < ApplicationCable::Channel
  after_subscribe :mark_online

  private

  # チャットに参加したユーザーを「オンライン」として記録できる。
  def mark_online
    current_user.update(online: true)
  end
end


before_unsubscribe(購読解除前)

・コンシューマーがチャネルの 購読を解除する前 に実行される処理。

[例: メッセージの未読情報を保存する。]

class ChatChannel < ApplicationCable::Channel
  before_unsubscribe :save_unread_messages

  private
  
  # チャットを離れる前に、最後に読んだメッセージの時間を記録できる。
  def save_unread_messages
    current_user.update(last_seen_at: Time.current)
  end
end


after_unsubscribe(エイリアス: on_unsubscribe)(購読解除後)

・クライアントがチャネルの 購読を解除した後 に実行される処理。

[例: ユーザーをオフラインリストに追加する。]

class ChatChannel < ApplicationCable::Channel
  after_unsubscribe :mark_offline

  private

  # ユーザーがチャットを離れたら「オフライン」として記録できる。
  def mark_offline
    current_user.update(online: false)
  end
end




クライアント側のコンポーネント

[コネクション]
  • クライアント(コンシューマー)がサーバーと WebSocket 接続 を確立するために必要。
  • Rails では Action Cable を使って WebSocket を扱うことができる。
  • bin/rails generate channelコマンドを実行すると、新しいチャネルを作成できる。
[コンシューマーの接続方法]
  • createConsumer()を呼び出すことで、WebSocketの接続を作成。
  • デフォルトでは、/cableに接続。
  • ただしサブスクリプション(購読)を1つ以上設定しないと接続は確立されない。
app/javascript/channels/consumer.js
// Action CableはRailsでWebSocketを扱うフレームワークを提供する
// WebSocketがある場所で`bin/rails generate channel`コマンドを使うと新しいチャネルを生成できる

// `@rails/actioncable` から `createConsumer` をインポート
import { createConsumer } from "@rails/actioncable"

// デフォルトの接続先(サーバーの `/cable`)に WebSocket を確立する
export default createConsumer()
オプションとして接続先のURLを指定する
  • wss:// → 暗号化されたWebSocket通信(推奨)
  • https:// → WebSocket over HTTP(一部環境で利用)
// 異なる接続先URLを指定する(WebSocketサーバーの接続先を指定)
createConsumer('wss://example.com/cable')

// または、WebSocket over HTTP を使う場合(HTTPS経由)
createConsumer('https://ws.example.com/cable')


  • localStorage から 認証トークン を取得し、URLに追加
  • 認証が必要なWebSocketサーバーに接続する場合に便利
  • セキュリティに注意(トークンを安全に管理する必要がある)
// 関数を使って接続先URLを動的に指定
createConsumer(getWebSocketURL)

function getWebSocketURL() {
  // ローカルストレージから認証トークンを取得
  const token = localStorage.getItem('auth-token')

  // トークンをクエリパラメータとして追加し、接続先URLを返す
  return `wss://example.com/cable?token=${token}`
}
[サブスクライバについて]
[サブスクライバ]


  • コンシューマー(Consumer) が チャネルを購読(サブスクライブ) すると、そのコンシューマーは サブスクライバ(Subscriber) になります。
  • consumer.subscriptions.create メソッドを使用して、特定のチャネルにサブスクリプションを作成します。
app/javascript/channels/chat_channel.js
import consumer from "./consumer"

// "ChatChannel" にサブスクライブ
// "room: 'Best Room'" を指定して、特定のルームにメッセージを受信・送信できるようにする
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })

//// app/javascript/channels/appearance_channel.js
import consumer from "./consumer"

// "AppearanceChannel" にサブスクライブ
// ユーザーのオンライン/オフライン状態の管理などに利用できる
consumer.subscriptions.create({ channel: "AppearanceChannel" })


  • コンシューマーは、特定のチャネルに対してサブスクライバ(購読者)として動作します。サブスクライブできる回数には制限がないため、1つのコンシューマーが複数のチャットルームに同時に参加することが可能です。
app/javascript/channels/chat_channel.js
import consumer from "./consumer"

// "ChatChannel" にサブスクライブ
// "1st Room" に参加し、メッセージの送受信が可能になる
consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })

// 同じ "ChatChannel" にもう1つのサブスクリプションを作成
// "2nd Room" にも同時に参加し、別のルームのメッセージも受信できる
consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" })




クライアント-サーバー間のやりとり

[ストリーム]
  • ストリームとは、パブリッシュされたコンテンツ(ブロードキャスト)をサブスクライバに配信する仕組みです。
  • 例えば、ライブ配信やチャットのメッセージが、登録している人たちにリアルタイムで送られるイメージです。
stream_from とは?
  • stream_fromは、文字列ベースのストリーム名でブロードキャストし、全員に一斉配信するのに向いている。(例: チャットルーム)
  • 以下のコードは、roomパラメータの値が"Best Room"の場合に、stream_fromを用いてchat_Best Roomという名前のブロードキャストをサブスクライブしています。
app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel

  # コンシューマーがこのチャネルをサブスクライブすると呼び出される
  def subscribed

    # "chat_#{params[:room]}" の名前のストリームを購読する
    # 例: params[:room] が "Best Room" の場合、"chat_Best Room" を購読
    stream_from "chat_#{params[:room]}"
  end
end
  • これで、Railsアプリケーションのどのコードでも、以下のようにbroadcastを呼び出せばチャットルームにブロードキャストできるようになります。
ActionCable.server.broadcast("chat_Best Room", { body: "このチャットルーム名はBest Roomです" })
stream_for とは?
  • stream_forは、特定のモデルのインスタンスに関連付けてストリームを作り、特定の対象にのみ配信するのに向いている。(例: 投稿のリアルタイム更新)
  • 以下のコードは、PostGlobalIDZ2lkOi8vVGVzdEFwcC9Qb3N0LzEの場合、stream_for postを呼ぶと、posts:Z2lkOi8vVGVzdEFwcC9Qb3N0LzEというストリームを購読する。
app/channels/posts_channel.rb
class PostsChannel < ApplicationCable::Channel

  # コンシューマーがこのチャネルをサブスクライブすると呼び出される
  def subscribed

    # params[:id] に基づいて対象の Post を取得
    post = Post.find(params[:id])

    # 取得した Post に対してストリームを開始
    # 例: Post の GlobalID が "Z2lkOi8vVGVzdEFwcC9Qb3N0LzE" の場合、
    # "posts:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE" という名前のストリームを購読
    stream_for post
  end
end
  • これで、以下のようにbroadcast_toを呼び出せばこのチャネルにブロードキャストできるようになります。
PostsChannel.broadcast_to(@post, @comment)
[ブロードキャスト]
  • ブロードキャスト(broadcasting)は、pub/subのリンクです。
  • パブリッシャーが送信した内容はブロードキャストを通じて、対応するチャネルのサブスクライバに直接届けられます。
  • 各チャネルは、0個以上のブロードキャストをストリーミングできます。
  • ブロードキャストはリアルタイム配信のみ対応し、後から接続したコンシューマーは過去の配信内容を受け取れません
[サブスクリプション]
  • あるチャネルサブスクライブされたコンシューマーは、サブスクライバになります。
  • 以下のコードのコネクションサブスクリプションと呼ばれます。
  • 受信メッセージは、Action Cableコンシューマーが送信するidに基いて、これらのチャネルサブスクライバにルーティングされます。
app/javascript/channels/chat_channel.js
import consumer from "./consumer"

// ChatChannelにサブスクライブ(room: "Best Room" を指定)
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {

  // メッセージを受信した際に呼ばれる関数
  received(data) {
    this.appendLine(data) // 受信データを追加
  },

  // チャットのメッセージをHTMLに追加する関数
  appendLine(data) {
    const html = this.createLine(data) // 受信データをHTML要素に変換
    const element = document.querySelector("[data-chat-room='Best Room']") // チャットルームの要素を取得
    element.insertAdjacentHTML("beforeend", html) // 取得した要素の末尾にメッセージを追加
  },

  // メッセージデータをHTMLの形式に変換する関数
  createLine(data) {
    return `
      <article class="chat-line">
        <span class="speaker">${data["sent_by"]}</span>  <!-- 発言者の名前 -->
        <span class="body">${data["body"]}</span>
      </article>
    `
  }
})       


[チャネルにパラメーターを渡す]
  • サブスクリプション作成時に、以下のようにクライアント側のパラメータをサーバー側に渡せます。
app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel

  # クライアントがサブスクライブしたときに実行されるメソッド
  def subscribed

    # 指定されたチャットルーム(room)のストリームを購読
    stream_from "chat_#{params[:room]}"
  end
end


  • subscriptions.createの第1引数に渡すオブジェクトは、そのAction Cableチャネルのparamsハッシュとして扱われます。
  • このオブジェクトにはchannelキーワードが必須です。
app/javascript/channels/chat_channel.js
import consumer from "./consumer"  // consumer(Action Cableの接続)をインポート

// ChatChannelをサブスクライブし、受信したデータを処理する
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {

  // メッセージを受信した時の処理
  received(data) {
    this.appendLine(data)  // 受信したデータをappendLineメソッドで処理
  },

  // メッセージを画面に追加するメソッド
  appendLine(data) {
    const html = this.createLine(data)  // データをHTMLに変換
    const element = document.querySelector("[data-chat-room='Best Room']")  // 該当する部屋のDOM要素を取得
    element.insertAdjacentHTML("beforeend", html)  // メッセージを追加
  },

  // データからHTMLの一行を生成するメソッド
  createLine(data) {
    return `
      <article class="chat-line">
        <span class="speaker">${data["sent_by"]}</span>  <!-- 送信者名 -->
        <span class="body">${data["body"]}</span>  <!-- メッセージ本文 -->
      </article>
    `
  }
})


  • このコードはアプリケーションのどこかで呼び出されます。(例えば新しいメッセージを通知する処理あたり)
# ActionCableを使って、特定のチャネルにメッセージをブロードキャスト
ActionCable.server.broadcast(

  "chat_#{room}",  # 送信先のチャネル名("chat_#{room}"は動的に変わる)
  {
    sent_by: "Paul",  # 送信者の名前(この場合「Paul」)
    body: "This is a cool chat app."  # メッセージの内容(この場合「This is a cool chat app.」)
  }
)


[メッセージを再ブロードキャストする]
  • クライアントが送信したメッセージを、他のクライアントにも共有する仕組みとして、再ブロードキャストがよく使われます。
  • 再ブロードキャストでは、送信元クライアントも含め、接続しているすべてのクライアントがメッセージを受信します。
  • paramsは、チャネルをサブスクライブするときと同じ内容で使用されます。


[Ruby側(サーバーサイド)]

app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel

  # ユーザーがチャネルにサブスクライブしたときに呼ばれる
  def subscribed

    # パラメータとして渡された `room` に基づき、指定のチャットルームをストリーム
    stream_from "chat_#{params[:room]}"
  end

  # チャネルから受信したデータを処理する
  def receive(data)

    # 受信したデータを同じチャットルームにブロードキャスト
    ActionCable.server.broadcast("chat_#{params[:room]}", data)
  end
end


[JavaScript側(クライアントサイド)]

app/javascript/channels/chat_channel.js
import consumer from "./consumer"

// ChatChannelを作成して、指定のルームにサブスクライブ
const chatChannel = consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {

  // サーバーから受け取ったデータを処理
  received(data) {

    // 受信したデータは `{ sent_by: "Paul", body: "This is a cool chat app." }` の形
  }
})

// サーバーにデータを送信する
chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })




フルスタックの例

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

例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/javascript/channels/appearance_channel.js
import consumer from "./consumer"

consumer.subscriptions.create("AppearanceChannel", {
  // サブスクリプション作成時に1度呼び出される
  initialized() {
    this.update = this.update.bind(this)
  },

  // サブスクリプションがサーバーで利用可能になると呼び出される
  connected() {
    this.install() // イベントリスナーを追加
    this.update() // ユーザーの状態を更新
  },

  // WebSocket接続がクローズすると呼び出される
  disconnected() {
    this.uninstall() // イベントリスナーを削除
  },

  // サブスクリプションがサーバーで却下されると呼び出される
  rejected() {
    this.uninstall() // イベントリスナーを削除
  },

  update() {
    this.documentIsActive ? this.appear() : this.away() // ユーザーの状態を判定し、適切なアクションを実行
  },

  appear() {
    // サーバーの`AppearanceChannel#appear(data)`を呼び出す
    this.perform("appear", { appearing_on: this.appearingOn }) // ユーザーがオンラインであることを通知
  },

  away() {
    // サーバーの`AppearanceChannel#away`を呼び出す
    this.perform("away") // ユーザーが離席中であることを通知
  },

  install() {
    window.addEventListener("focus", this.update) // ウィンドウがアクティブになったときに更新
    window.addEventListener("blur", this.update) // ウィンドウが非アクティブになったときに更新
    document.addEventListener("turbo:load", this.update) // Turboナビゲーションの読み込み時に更新
    document.addEventListener("visibilitychange", this.update) // ページの可視状態が変化したときに更新
  },

  uninstall() {
    window.removeEventListener("focus", this.update) // フォーカスイベントを削除
    window.removeEventListener("blur", this.update) // ブラーイベントを削除
    document.removeEventListener("turbo:load", this.update) // Turboナビゲーションのイベントを削除
    document.removeEventListener("visibilitychange", this.update) // 可視状態の変更イベントを削除
  },

  get documentIsActive() {
    return document.visibilityState === "visible" && document.hasFocus() // ページが可視かつフォーカスされているか判定
  },

  get appearingOn() {
    const element = document.querySelector("[data-appearing-on]") // `data-appearing-on` 属性を持つ要素を取得
    return element ? element.getAttribute("data-appearing-on") : null // 属性の値を取得し、なければ `null` を返す
  }
})


[クライアントとサーバー間のやり取り]

1. クライアントがサーバーに接続する

  • クライアントは、createConsumer()を使ってサーバーに接続します。(consumer.js)
  • サーバー側では、この接続をcurrent_userを使って識別します。

2. クライアントがアピアランスチャネルに接続する

  • クライアントは、consumer.subscriptions.create({ channel: "AppearanceChannel" })を実行し、アピアランスチャネルに接続します。(appearance_channel.js)

3. サーバーがサブスクリプションを認識し、ユーザーをオンライン状態にする

  • クライアントがアピアランスチャネルに接続すると、サーバーは新しいサブスクリプションを認識し、subscribed コールバックを実行します。(appearance_channel.rb)
  • その後、サーバーは current_userappearメソッドを呼び出し、ユーザーを「オンライン」として登録します。

4. クライアントがサブスクリプションの確立を認識し、オンライン状態を通知する

  • クライアントは、サブスクリプションが確立したことを検知し、connectedメソッドを実行します。(appearance_channel.js)
  • そうすると、installメソッド(イベントリスナーの登録)と appearメソッド(サーバーへの通知)が呼び出されます。
  • appearメソッドは、サーバーの AppearanceChannel#appear(data)を実行し、{ appearing_on: this.appearingOn }のデータを送信します。
  • これは、RailsのAction Cableでは、クラス内の(コールバック以外の)すべてのパブリックメソッドがサーバー側のチャネルインスタンスから自動的に公開され、performメソッドを使ってRPC(リモートプロシージャコール)として実行できるためです。

5. サーバーがリクエストを受け取り、ユーザーのオンライン状態を処理する

  • サーバーは、current_user に紐づく接続のアピアランスチャネルで appearメソッドのリクエストを受け取ります。(appearance_channel.rb)
  • 送信されたデータの :appearing_on キーを取り出し、その値を current_user.appearon キーに渡して、ユーザーのオンライン状態を更新します。


例2: 新しいWeb通知を受信する

[Web通知機能のリアルタイム通信の仕組み]
  • 受信した情報を基に、自動的にWeb通知を表示する仕組みを実装します。
  • この例は、WebSocketを使用してサーバーからクライアントの機能をリモートで実行し、その動作を制御する方法です。
  • WebSocketは双方向通信が可能なため、サーバー側からクライアントのアクションを起動できます。
  • 具体的には、「Web通知チャネル」を利用し、特定のストリームにブロードキャストされた情報をクライアント側で受信します。


[サーバー側のWeb通知チャネル]

app/channels/web_notifications_channel.rb
class WebNotificationsChannel < ApplicationCable::Channel
  # クライアントがこのチャンネルに接続したときに呼び出されるメソッド
  def subscribed
    # current_user(現在のユーザー)専用のストリームを作成
    # これにより、サーバー側から current_user に向けた通知を送ることができる
    stream_for current_user
  end
end


[クライアント側のWeb通知]

app/javascript/channels/web_notifications_channel.js
// クライアント側では、サーバーからのWeb通知を受信するために
// ブラウザの通知許可をリクエスト済みであることが前提

import consumer from "./consumer"

// "WebNotificationsChannel" に対するサブスクリプションを作成
consumer.subscriptions.create("WebNotificationsChannel", {
  // サーバーからデータを受信したときに実行される処理
  received(data) {
    // Web通知を作成し、データに含まれるタイトルと本文を表示
    new Notification(data["title"], { body: data["body"] })
  }
})


[WebNotificationsChannel.broadcast_toの動作について]
  • 以下のように、アプリケーションのどこからでもWeb通知チャネルのインスタンスにコンテンツをブロードキャストできます。
# どこかで呼び出される処理((例: 新しいメッセージが投稿されたとき)
WebNotificationsChannel.broadcast_to(
  current_user, # 通知を送る対象のユーザー(current_user に対して送信)
  title: "新着情報!", # 通知のタイトル
  body: "印刷しておきたいニュース記事リスト" # 通知の本文
)
コードの解説
  • broadcast_toを呼び出すと、現在のサブスクリプションアダプタpub/subキュー にメッセージを設定する。

  • ユーザーごとに異なるブロードキャスト名が使われる。例として、ユーザーIDが 1の場合、ブロードキャスト名は web_notifications:1になります。

  • WebNotificationsChannelは、web_notifications:1 で受信したメッセージを、receivedコールバックを通じてクライアントへストリーミングする。

  • クライアント側で受信するデータは、サーバー側のbroadcast_toの第2引数として渡されたハッシュです。

  • このハッシュはJSONにエンコードされ、クライアントのreceived(data)で利用できる。

  • JSONにエンコード」とは、データをJSON(JavaScript Object Notation)というフォーマットに変換することを指します。




設定

  • Action Cableで必須となる設定は、「サブスクリプションアダプタ」「許可されたリクエスト送信元」の2つです。
[サブスクリプションアダプタ]
  • Action Cableは、デフォルトでconfig/cable.ymlの設定ファイルを利用します。
  • Railsの環境ごとに、アダプタとURLを1つずつ指定する必要があります。
ddevelopment: # 開発環境
  adapter: postgresql  # PostgreSQLアダプタを利用
  url: postgres://user:password@localhost:5432/my_database  # 接続情報

test: # テスト環境
  adapter: test  # テスト専用アダプタ(実際の接続は行わずにテストを実施)

production: # 本番環境
  adapter: postgresql  # PostgreSQLアダプタを利用
  url: postgres://user:password@db.example.com:5432/my_database  # 本番DBの接続先
  channel_prefix: appname_production  # チャンネル名のプレフィックス(識別用)
利用できるアダプタ設定について

[エンドユーザー向けに利用できるサブスクリプションアダプタの一覧]

  • 7.1.1.1 Asyncアダプタ
  • 7.1.1.2 Redisアダプタ
  • 7.1.1.3 PostgreSQLアダプタ


[7.1.1.3 PostgreSQLアダプタについて]

  • Active Recordのコネクションプールを利用し、効率的に接続を管理する。
  • config/database.ymlで設定し、データベースの接続情報を指定する。
  • 将来的に仕様変更の可能性あるので、最新のRailsドキュメントを確認するのが推奨される。


[許可されたリクエスト送信元]
  • Action Cableは、指定されていない送信元からのリクエストを受け付けません。
  • 送信元リストをサーバー設定に配列の形で渡す必要があります。
  • リクエストの送信元がリスト内の条件と一致するかどうかがチェックされます。
  • デフォルトでは、development環境で実行中のAction Cableは、localhost:3000からの全てのリクエストを許可します。


[許可されたリクエスト送信元を指定する場合]

config.action_cable.allowed_request_origins = [
  "https://rubyonrails.com", # ① このオリジン(送信元URL)を許可
  %r{http://ruby.*}          # ② "http://ruby" で始まるURLを正規表現で許可
]


[全ての送信元からのリクエストを許可、または拒否する場合]

config.action_cable.disable_request_forgery_protection = true


[コンシューマーの設定]


[URLの設定方法]

  • HTML<head>セクションにaction_cable_meta_tagを追加します。
  • これにより、Action CableURLを設定できます。

[設定の適用場所]

  • 通常は環境設定ファイル (config/environments/***.rb) で指定します。
  • config.action_cable.urlにURLやパスを設定します。


[ワーカープールの設定]


[ワーカープールとは?]

  • ワーカープールとは、処理を分担するために複数のワーカー(作業者)を用意し、それらを効率よく管理する仕組みです。具体的には、あるタスクを実行するためのスレッドプロセスプール(集めて管理)しておき、必要に応じてそれらを使い回します。

  • サーバーのメインスレッドから隔離された状態でコネクションのコールバックやチャネルのアクションを実行するために用いられます。


[設定方法]

config.action_cable.worker_pool_size = 4 # デフォルトではワーカープールのサイズは 4 に設定されている。
データベースコネクションについて
  • サーバーのデータベースコネクション数は、ワーカー数以上にする必要があります。
  • 例: ワーカー数が 4 なら、データベースのコネクション数も最低 4以上に設定します。
  • データベースのコネクション数は、config/database.ymlpool属性で変更可能です。


[クライアント側のログ出力]
  • クライアント側のログ出力は無効になっています。
  • ActionCable.logger.enabledtrueを設定することで、ログ出力を有効にできます。
  • クライアント側でWebSocket通信状況やエラーを確認できるようになります。
// RailsのAction Cableライブラリをインポートする
import * as ActionCable from '@rails/actioncable'

// クライアント側のログ出力を有効にする
// これをtrueにすると、WebSocketの接続状態やメッセージの送受信がコンソールに表示される
ActionCable.logger.enabled = true


[その他の設定]
  • その他によく使われるオプションとして、コネクションごとのロガーにタグを保存するオプションがあります。
  • 以下の例は、ユーザーアカウントidがある場合はそれをタグ名にし、ない場合は「no-account」をタグ名にします。
config.action_cable.log_tags = [
  # リクエストオブジェクトから "user_account_id" を取得し、存在しない場合は "no-account" を設定
  -> request { request.env["user_account_id"] || "no-account" },

  # 固定のタグ ":action_cable" を設定
  :action_cable,

  # リクエストの UUID をタグとして設定
  -> request { request.uuid }
]




以上で Action Cable についての覚書を修了します。






GitHubで編集を提案

Discussion