Action Cableについて
開発環境
- macOS
- VSCode
- Rails 7.1.3.3
- ruby-3.2.3
行いたいこと
- RailsガイドのAction Cableについて、自分なりにわかりやすいように噛み砕いたので覚書として記載します。
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::Connection
はActionCable::Connection::Base
を継承しています。 -
ApplicationCable::Connection
では、ユーザーを識別できる場合に限り、認証後に接続を確立します。
[コネクションの設定]
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
コードの解説
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
を使う方法
module ApplicationCable
class Connection < ActionCable::Connection::Base
rescue_from StandardError, with: :report_error
private
def report_error(e)
SomeExternalBugtrackingService.notify(e)
end
end
end
- コードの解説
# デフォルトでは、捕捉されなかった例外は 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クラスの解説
・このコードはRails
のAction Cable
における接続クラスの定義です。
・ActionCable::Connection::Base
を継承してWebSocket
接続を管理します。
・主な機能は例外処理(エラーハンドリング)
の設定です。
例外処理機能
・rescue_fromメソッド
を使用して例外を捕捉しています。
・StandardError(ほとんどの一般的なエラー)
が対象です。
・エラー発生時はreport_error メソッド
が呼び出されます。
report_errorメソッド
・捕捉したエラーを外部サービスに通知する
役割があります。
・引数 e
として捕捉された例外オブジェクトを受け取ります。
・SomeExternalBugtrackingService.notify(e)
でエラー情報を送信します。
・実際の実装ではSentry
やBugsnag
などの実際のサービス名が使われます。
設計上のポイント
・プライベートメソッド
として定義されているため、外部からは直接呼び出せません。
・Action Cable 全体でのグローバルなエラーハンドリング
を実現しています。
・エラーを記録するだけでなく、外部サービスへの通知
も行います。
チャネル(Channel)
[チャネルとは]
-
チャネル(Channel)
は、特定の機能や処理をまとめる単位であり、リアルタイム通信の処理を行う場所です。 -
MVC
設計パターンの「コントローラ
」に似た役割を果たし、クライアントとサーバー間でやり取りするデータを管理します。 -
Action Cableの
コア機能の一つ
であり、WebSocket接続内で特定の処理を行うために使用されます。
[ApplicationCable::Channel について]
-
チャネルジェネレータ
を初めて使うと自動的に作成されます。(プロジェクトで最初にrails generate channel
を実行したときに作成される) -
ApplicationCable::Channel
はActionCable::Channel::Base
を継承し、各チャネルで共通する処理を定義できる親クラスです。 -
複数のチャネル間で共有される
ロジックをカプセル化
する親クラスです。
[システム構造における位置づけ]
-
チャネル
は、WebSocketを通じてリアルタイム通信を処理するためのロジックを実装する場所
です。 - 複数のチャネルで共通の機能は
親クラス(ApplicationCable::Channel)
に配置し、各チャネルはそれを継承することで共通の処理を利用できます。 -
MVCの考え方をリアルタイム通信にも適用し
、クライアントとサーバー間のイベント管理を分離しやすくする構造になっています。
[親チャネルと個別チャネルの設計]
- 以下の記載で
クライアント側からコンシューマーがチャネルをサブスクライブ
できるようになります。(ブラウザなどがWebSocketを通じて特定のチャネルに接続し、リアルタイム通信を利用できるようになります)
ApplicationCable::Channel すべてのチャネルの親クラス(親チャネルの設定)
module ApplicationCable
# 役割: 共通機能を提供し、すべての独自チャネルがこれを継承する
# ActionCable::Channel::Baseを継承して基本的なチャネル機能を取得
class Channel < ActionCable::Channel::Base
end
end
ChatChannel - チャット機能専用のチャネル
# 役割: チャットメッセージの送受信、プレゼンス管理などの
# チャット関連のビジネスロジックを実装する
# ApplicationCable::Channelを継承して共通機能を取得
class ChatChannel < ApplicationCable::Channel
end
AppearanceChannel - ユーザーのオンライン状態を管理するチャネル
# 役割: ユーザーのログイン状態、オンライン/オフライン状態の変更通知、
# アクティビティステータスなどを管理
# ApplicationCable::Channelを継承して共通機能を取得
class AppearanceChannel < ApplicationCable::Channel
end
[サブスクリプション(Subscription)]
-
コンシューマー(Consumer)
はチャネルをサブスクライブ(Subscribe)
し、サブスクライバ(Subscriber)
の役割を果たす。 -
コンシューマーのコネクション
はサブスクリプション(Subscription: 購読)
と呼ばれる。 - 生成されたメッセージは、
Action Cableのコンシューマー(クライアント)
が送信するid
をもとに適切な宛先へ送られます。
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
を利用すると特定チャネルで発生する例外を扱えるようになります。
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つ以上設定しないと接続は確立されない。
// 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 メソッド
を使用して、特定のチャネルにサブスクリプションを作成します。
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つのコンシューマーが複数のチャットルームに同時に参加する
ことが可能です。
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
という名前のブロードキャストをサブスクライブ
しています。
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
は、特定のモデルのインスタンスに関連付けてストリームを作り、特定の対象にのみ配信する
のに向いている。(例: 投稿のリアルタイム更新) - 以下のコードは、
Post
のGlobalID
がZ2lkOi8vVGVzdEFwcC9Qb3N0LzE
の場合、stream_for post
を呼ぶと、posts:Z2lkOi8vVGVzdEFwcC9Qb3N0LzE
というストリームを購読する。
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
に基いて、これらのチャネルサブスクライバにルーティングされます。
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>
`
}
})
[チャネルにパラメーターを渡す]
-
サブスクリプション作成時
に、以下のようにクライアント側のパラメータをサーバー側
に渡せます。
class ChatChannel < ApplicationCable::Channel
# クライアントがサブスクライブしたときに実行されるメソッド
def subscribed
# 指定されたチャットルーム(room)のストリームを購読
stream_from "chat_#{params[:room]}"
end
end
-
subscriptions.create
の第1引数に渡すオブジェクトは、そのAction Cableチャネルのparamsハッシュ
として扱われます。 - このオブジェクトには
channelキーワード
が必須です。
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側(サーバーサイド)]
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側(クライアントサイド)]
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: ユーザーアピアランスの表示
[アピアランスチャネル]
-
アピアランス
とは、ユーザーがオンラインかどうか、またどのページを開いているか
を追跡するための仕組みのことです。 - 例として、オンラインユーザーの
名前の横に緑の点を表示する機能
を作る際に役立ちます。 - 以下は、
ユーザーがオンラインかどうか
、またどのページを開いているか
を追跡するためのチャネルの例です。
[サーバー側のアピアランスチャネルのコード]
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やデータベースと連携させることも可能です。
[クライアント側のアピアランスチャネルのコード]
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_user
のappearメソッド
を呼び出し、ユーザーを「オンライン」として登録します。
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.appear
のon キー
に渡して、ユーザーのオンライン状態を更新します。
例2: 新しいWeb通知を受信する
[Web通知機能のリアルタイム通信の仕組み]
- 受信した情報を基に、
自動的にWeb通知を表示する仕組み
を実装します。 - この例は、
WebSocket
を使用してサーバーからクライアントの機能をリモートで実行し、その動作を制御する方法です。 - WebSocketは
双方向通信
が可能なため、サーバー側からクライアントのアクションを起動できます。 - 具体的には、「
Web通知チャネル
」を利用し、特定のストリームにブロードキャストされた情報をクライアント側で受信します。
[サーバー側のWeb通知チャネル]
class WebNotificationsChannel < ApplicationCable::Channel
# クライアントがこのチャンネルに接続したときに呼び出されるメソッド
def subscribed
# current_user(現在のユーザー)専用のストリームを作成
# これにより、サーバー側から current_user に向けた通知を送ることができる
stream_for current_user
end
end
[クライアント側のWeb通知]
// クライアント側では、サーバーからの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 Cable
のURL
を設定できます。
[設定の適用場所]
- 通常は
環境設定ファイル (config/environments/***.rb)
で指定します。 -
config.action_cable.url
にURLやパスを設定します。
[ワーカープールの設定]
[ワーカープールとは?]
-
ワーカープール
とは、処理を分担するために複数のワーカー(作業者)
を用意し、それらを効率よく管理する仕組みです。具体的には、あるタスクを実行するためのスレッド
やプロセス
をプール(集めて管理)
しておき、必要に応じてそれらを使い回します。 -
サーバーの
メインスレッドから隔離
された状態でコネクションのコールバックやチャネルのアクションを実行
するために用いられます。
[設定方法]
config.action_cable.worker_pool_size = 4 # デフォルトではワーカープールのサイズは 4 に設定されている。
データベースコネクションについて
- サーバーの
データベースコネクション数
は、ワーカー数以上
にする必要があります。 - 例:
ワーカー数が 4
なら、データベースのコネクション数も最低 4以上
に設定します。 - データベースのコネクション数は、
config/database.yml
のpool属性
で変更可能です。
[クライアント側のログ出力]
- クライアント側の
ログ出力は無効
になっています。 -
ActionCable.logger.enabled
にtrue
を設定することで、ログ出力を有効
にできます。 - クライアント側で
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 についての覚書を修了します。
Discussion