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