Perfect Ruby on Rails ActionCable - wahei628/ShareSuke GitHub Wiki
ActionCable/WebScoketsとは
- ActionCable
[Rails 7でリアルタイム通信を実現! Action Cableの基本をチュートリアルとともに理解しよう](https://codezine.jp/article/detail/17960)
ActionCableはWebSocketを使ったリアルタイム処理を提供するライブラリ。
ActionCable用のファイルを作成するには下記のコマンドを実行する
Rails g channel room speak
下記のファイルが作成される。
- app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
# クライアントがこのチャンネルにサブスクライブ(接続)
# したときに呼び出されるメソッド
end
def unsubscribed
# クライアントがこのチャンネルからサブスクリプションを解除(切断)
# したときに呼び出されるメソッド
end
def speak
# クライアントからの特定のアクションを処理するために使用される。
# この例ではメソッドの本体が空ですが、例えばクライアントから送信された
# メッセージを処理するロジックをここに記述
end
end
- app/javascript/channels/room_channel.js
import consumer from "channels/consumer"
// consumerをchannels/consumerからインポートしています。
// このconsumerは、ActionCableのコンシューマ(クライアント)
// インスタンスを表します。これを使ってチャンネルのサブスクリプションを作成
consumer.subscriptions.create("RoomChannel", {
// RoomChannelという名前のチャンネルに対するサブスクリプションを作成
// 第2引数にはサブスクリプションの設定オブジェクトを渡します
connected() {
// サブスクリプションがサーバーで準備完了したときに呼び出されるメソッド
//このメソッド内で、接続が確立されたときの処理を記述します
},
disconnected() {
// サブスクリプションがサーバーで終了されたときに呼び出されるメソッド。
// このメソッド内で、接続が切断されたときの処理を記述
},
received(data) {
// このチャンネルでWebSocketからデータを受信したときに呼び出されるメソッド
// サーバーから送られてきたデータは、引数dataとして渡される。
// このメソッド内で、受信したデータを処理する
},
speak: function() {
// speakという名前のメソッドを定義。このメソッドは、performメソッド
// を使ってサーバー側のspeakアクションを呼び出します。
return this.perform('speak');
// クライアントからこのメソッドが呼ばれると、サーバーに「speak」
// アクションを実行するようにリクエストが送られます。
}
});
Rails g channelコマンドでActionCable用のファイルを作成した後
サーバーを再起動しブラウザにアクセスするとWebSocketでの接続ができる。
- app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
# クライアントがサブスクライブしたときに、room_channelという
# 名前のブロードキャストストリームを購読するためのメソッド
# このストリームにブロードキャストされたメッセージをクライアントが受信できるようになる。
end
def unsubscribed
end
def speak
ActionCable.server.broadcast(
# ActionCable.server.broadcast: このメソッドを使って、特定のチャンネルに
# メッセージをブロードキャストする。この場合、room_channelという名前のチャンネルにメッセージを送信。
"room_channel", { message: data["message"] }
# room_channelという名前のチャンネルに対して、
# messageキーにdata["message"]の値を持つハッシュをブロードキャストします。
# data["message"]は、クライアントから送信されたメッセージです。
)
end
end
クライアントからメッセージが届くとActionCable.server.broadcastメソッドを使ってストリーム名room_channelで通信するクライアント全てに、届いたメッセージを送信する。
- ブロードキャストとは?
- app/javascript/channels/room_channel.js
import consumer from "channels/consumer";
window.App = consumer.subscriptions.create("RoomChannel", {
// コンソールから呼び出せるようにwindow.Appに代入
connected() {},
disconnected() {},
received(data) {
// サーバーからデータを受け取った時に呼び出されるメソッド
alert(data["message"]);
// 受け取ったデータのmessageプロパティをアラート表示
},
speak: function (message) {
// サーバーサイドへメッセージを送信するメソッド
//引数messageを取り、speakアクションに対してこのメッセージを含むリクエスト送る
return this.perform("speak", { message: message });
},
});
クライアント側のコードで、**speak
**メソッドが呼び出されると、メッセージがサーバーに送信されます。
window.App.speak("Hello, World!");
この呼び出しにより、以下のコードが実行されます:
speak: function (message) {
return this.perform("speak", { message: message });
}
ここでは、**perform
メソッドがRoomChannel
のspeak
アクションを呼び出し、message
**を含むデータをサーバーに送信します。
2. サーバーサイドのspeakメソッドが呼び出され、引数(data)を取得し、「room_channel」という名前のストリームにdataのmessageプロパティを変数messageとしてクライアント側へブロードキャスト送信
サーバーサイドでは、**RoomChannel
のspeak
**メソッドが呼び出され、データが処理されます。
class RoomChannel < ApplicationCable::Channel
def speak(data)
ActionCable.server.broadcast("room_channel", { message: data["message"] })
end
end
このメソッドは、**data
引数からmessage
プロパティを取り出し、そのメッセージをroom_channel
**ストリームにブロードキャストします。
クライアント側では、**received
**メソッドが呼び出され、サーバーから送信されたデータを受け取ります。
received(data) {
alert(data["message"]);
}
ここでは、**data
のmessage
**プロパティを取り出し、アラートで表示します。
フォームから文字を入力してブラウザ上で確認できる様にしていく
- app/views/rooms/show.html.erb
<h1> chatroom </h1>
<div id = "message">
<%= render @messages %>
</div>
<form>
<label> Say Something </label>
<input type = "text" data-behavior = "room_speaker">
</form>
data-behavior属性はJavaScript側で、入力されたテキストを取得するための識別子として利用する。
クライアントサイドの送受信処理を実装
- app/javascript/channels/room_channel.js
import consumer from "channels/consumer";
consumer.subscriptions.create("RoomChannel", {
connected() {
document.querySelector('input[data-behavior="room_speaker"]')
.addEventListener("keypress", (event) => {
if (event.key == "Enter") {
this.speak(event.target.value);
event.target.value = "";
return event.preventDefault();
}
});
},
disconnected() {},
received(data) {
const element = document.querySelector("#message");
element.insertAdjacentHTML("beforeend", data["message"]);
},
speak: function (message) {
return this.perform("speak", { message: message });
},
});
- insertAdjacentHTMLとは?
[innerHTML より insertAdjacentHTML を使う - Qiita](https://qiita.com/amamamaou/items/624c22adec32515e863b)
document.querySelector('input[data-behavior="room_speaker"]')
.addEventListener("keypress", (event) => {
if (event.key == "Enter") {
this.speak(event.target.value);
event.target.value = "";
return event.preventDefault();
}
});
- document.querySelector('input[data-behavior="room_speaker"]'):
ページ内の**data-behavior
属性が"room_speaker"
であるinput
**要素を取得します。
querySelectorとは?
[要素を取得する3つのJSメソッド 〜挙動の違いをまとめてみた〜](https://zenn.dev/harryduck/articles/e3e6c9d37e0169c05096)
-
addEventListener("keypress", (event) => {...}): 取得した**
input
要素に対してkeypress
**イベントリスナーを追加します。ユーザーがキーを押したときに実行されます。 - if (event.key == "Enter"): 押されたキーが"Enter"キーであるかどうかを確認します。
-
this.speak(event.target.value): **
speak
メソッドを呼び出し、input
**要素の値を引数として渡します。これにより、ユーザーが入力したメッセージがサーバーに送信されます。 -
event.target.value = "": メッセージ送信後に**
input
**要素の値を空にします。 -
return event.preventDefault(): **
Enter
**キーのデフォルトの動作を防止します。これにより、フォームの送信や改行が行われません。
preventDefault()とは?
[【JavaScript】event.preventDefault()が何をするのか - Qiita](https://qiita.com/yokoto/items/27c56ebc4b818167ef9e)
メッセージをデータベースに保存し、部分テンプレートから生成したHTMLを送信する様にする
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
# ストリーム名を固定にして、DBから保存しているメッセージを取得すれば
# 半永久的にデータは残りそう
end
def unsubscribed
end
def speak(data)
message = Message.create!(content: data["message"])
ActionCable.server.broadcast(
"room_channel", { message: render_message(message) }
)
end
private
def render_message(message)
ApplicationController.render(
partial: "messages/message",
locals: { message: message }
)
end
end
**ApplicationController.render
**メソッドは、Railsアプリケーションでコントローラのコンテキスト外でビューや部分テンプレートをレンダリングするために使用されるメソッドです。このメソッドを使用すると、通常のコントローラアクションからではなく、任意の場所からビューをレンダリングすることができます。
- バックグラウンドジョブでメールのプレビューを作成する
- サービスオブジェクトやモデルのコールバックで部分テンプレートをレンダリングする
- WebSocketを介してリアルタイムで更新されるデータをレンダリングする
**ApplicationController.render
**メソッドのシグネチャは次の通りです:
ApplicationController.render(options = {}, assigns = {}, &block)
-
:template
- レンダリングするテンプレートのパスを指定します。例えば、"users/show"
。 -
:partial
- レンダリングする部分テンプレートのパスを指定します。例えば、"users/user"
。 -
:locals
- テンプレートに渡すローカル変数のハッシュを指定します。例えば、{ user: @user }
。 -
:formats
- テンプレートのフォーマットを指定します。例えば、[:html, :json]
。 -
:handlers
- テンプレートのハンドラを指定します。例えば、[:erb, :haml]
。 -
:layout
- レイアウトテンプレートを指定します。例えば、"layouts/application"
。
- config/cable.yml
development:
adapter: async
# 非同期アダプターを使用することを指定。非同期アダプターは、
# 開発環境で動作するシンプルなアダプターで、外部のサービス(例えばRedis)を必要としない
# インメモリで接続情報を管理
test:
adapter: test
production:
adapter: redis
url: <%= ENV.fetch("REDIS_URL") { "redis://localhost:6379/1" } %>
# 環境変数REDIS_URLの値を取得。環境変数が設定されていない場合は、
# デフォルトでredis://localhost:6379/1を使用する。このURLは、ローカルマシンのRedisサーバーに接続するためのものです。
# 6379はRedisのデフォルトポートで、1は使用するデータベース番号を示します。
channel_prefix: actioncable_sample_production
**config/cable.yml
**ファイルは、Railsアプリケーションの3つの異なる環境(development、test、production)ごとにActionCableの設定を定義します。
- channel_prefix: actioncable_sample_production
channel_prefix
は、ActionCableがRedisに格納するデータのキーにプレフィックス(接頭辞)を追加するための設定です。これにより、同じRedisサーバーを共有する複数のRailsアプリケーションや異なる環境のアプリケーション間でデータの競合を防ぐことができます。
例えば、channel_prefix
を actioncable_sample_production
と設定した場合、ActionCableはRedisにメッセージを保存する際に、このプレフィックスをキーに追加します。
もし**channel_prefix
**が設定されていない場合、デフォルトのキーは次のようになります:
action_cable/
action_cable/channels/
action_cable/broadcasting/
これらのキーは、Redis内で他のアプリケーションや同じアプリケーションの異なる環境と競合する可能性があります。
一方、**channel_prefix: actioncable_sample_production
**が設定されている場合、キーは次のようにプレフィックス付きで保存されます:
actioncable_sample_production:action_cable/
actioncable_sample_production:action_cable/channels/
actioncable_sample_production:action_cable/broadcasting/
これにより、同じRedisサーバーを共有していても、このアプリケーションのデータは他のデータと混ざらないようになります。
Rackは、RubyのWebサーバーとWebアプリケーションを接続するためのインターフェースです。簡単に言えば、Webサーバー(Puma、Unicorn、WEBrickなど)とWebアプリケーション(Rails、Sinatraなど)の間の「接着剤」として機能します。Rackは、Webリクエストを処理し、レスポンスを返すためのシンプルで一貫した方法を提供します。
デフォルトでは一つのプロセスにWEB用のアプリケーション、WebSockert用のアプリケーションが混在しているので、下記のような問題に遭遇することがある。
- リソースの競合:
同じプロセスでHTTPリクエストとWebSocket接続を処理すると、リソースが共有され、競合が発生します。
- スケーラビリティの問題:
異なる特性を持つ処理を同じプロセスで行うと、スケーリングが難しくなります。
- 応答時間の遅延:
WebSocket接続の増加により、通常のHTTPリクエストの応答時間が遅れる可能性があります。
これらの問題を避けるために、WebサーバーとWebSocketサーバーを分離し、それぞれを異なるプロセスやサーバーで役割を分けるのが良い。
-
Webサーバー:
- 通常のHTTPリクエストを処理します(例:Webページの読み込みやAPI呼び出し)。
- 例:NginxやApacheを使用。
-
WebSocketサーバー:
- WebSocket接続を処理します(例:リアルタイムチャットのメッセージ送受信)。
- 例:専用のPumaサーバーやActionCable専用サーバーを使用。
- cable/config.ru
require_relative "../config/environment"
# ../config/environment" によって、Railsアプリケーションの環境が設定され、
# 初期化プロセスが実行されます。
Rails.application.eager_load!
# eager_load することで、アプリケーションのコードがメモリに事前に読み込まれ、
# 最初のリクエストが処理される際のパフォーマンスが向上します
run ActionCable.server
スタンドアローン環境でActionCableを使用する場合は、
Web用のサーバーにActionCableがマウントされているのを解除する事
下記ファイルのコメントアウトを解除すれば良い。
- config/environments/production.rb
config.action_cable.mount_path = nil
ActionCable専用のサーバープロセスを立ち上げた場合、一般的には
別のサブドメインで運用することになる。
クライアントからのActionCableへの接続はデフォルトではlocalhostが指定されているので変更する必要がある。
- config/environments/production.rb
config.action_cable.url = "wss://example.com/cable"
RailsアプリケーションのActionCableのWebSocket接続先URLを指定。
具体的には、クライアントがWebSocket接続を確立するために使用するURLを設定しています。
WebSocketは、双方向のリアルタイム通信を可能にするプロトコルです。通常のHTTPリクエスト/レスポンスのモデルとは異なり、WebSocketではクライアントとサーバーが継続的な接続を確立し、その接続を通じてデータを送受信します。
-
ws: WebSocketプロトコルのスキーム。通常のWebSocket接続を表します。
ws://
で始まるURLは、暗号化されていないWebSocket接続を示します。
WebSocket Secure(WSS)は、WebSocketプロトコルの暗号化されたバージョンです。これにより、データの送受信がTLS(Transport Layer Security)またはSSL(Secure Sockets Layer)によって保護されます。
-
wss: WebSocket Secureプロトコルのスキーム。暗号化されたWebSocket接続を表します。
wss://
で始まるURLは、暗号化されたWebSocket接続を示します。
-
セキュリティ:
- データの盗聴防止: WSSを使用すると、WebSocket通信が暗号化されるため、第三者が通信内容を盗聴することができません。
- データの改ざん防止: TLS/SSLにより、通信データの改ざんが防止されます。
-
信頼性:
- セキュリティの向上: 多くのネットワークでは、暗号化されていない通信をブロックすることが一般的です。WSSを使用することで、ファイアウォールやプロキシサーバーによるブロックを回避できます。
JavaScriptmの読み込みタグの前にaction_cable_meta_tagを追加
クライアントサイドのActionCableはaction_cable_meta_tagによって生成されたタグを見て接続先を判別する。
- app/views/layouts/application.html.erb
<!DOCTYPE html>
<html>
<head>
<title>ActioncableSample</title>
<meta name="viewport" content="width=device-width,initial-scale=1">
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
+ <%= action_cable_meta_tag %>
<%= stylesheet_link_tag "application", "data-turbo-track": "reload" %>
<%= javascript_importmap_tags %>
</head>
<body>
<%= yield %>
</body>
</html>
上記で生成されるメタタグの例
<meta name="action-cable-url" content="ws://localhost:3000/cable">
このメタタグは、WebSocket接続のURL(例えば、ws://localhost:3000/cable
)を指定します。ここで**ws
はWebSocketを示し、localhost:3000
はサーバーのホスト名とポート、/cable
**は接続先のエンドポイントです。
- オリジンとは?
オリジンは、Webリクエストがどこから来たのかを示す情報で、以下の3つの要素から構成されます:
-
URIスキーム: 通信プロトコル(例:
http
、https
) -
ホスト: ドメイン名またはIPアドレス(例:
localhost
、example.com
) -
ポート番号: サーバーが使用するポート(例:
3000
、443
)
例えば、**http://localhost:3000
**というURLのオリジンは、以下のように分解されます:
- スキーム:
http
- ホスト:
localhost
- ポート番号:
3000
ActionCableはセキュリティのために、WebSocket接続を許可するオリジンを制限します。これにより、許可されたオリジンからの接続のみを受け付け、不正なオリジンからの接続を防ぎます。
Railsの開発環境(development)では、以下のような正規表現が自動的に設定されており、http://localhost:3000
のようなURLからの接続が許可されます:
/https?:\/\/localhost:\d+/
この正規表現の意味は以下の通りです:
-
https?
:http
またはhttps
のスキームを許可します。 -
:\/\/localhost
:localhost
ホストを許可します。 -
\d+
: 任意のポート番号(1桁以上の数字)を許可します。
開発環境以外で、例えば別のドメイン(example.com
)から接続を許可する場合や、本番環境(production)で接続を許可する場合は、明示的にオリジンを設定する必要があります。
- config/environments/production.rb
config.action_cable.allowed_request_origins = [ "http://example.com", /http:\/\/example.*/
これは、ActionCableが許可するオリジンのリストを設定するためのオプションです。
配列として設定し、許可するオリジンを文字列で指定します。
上記までで作成したチャットアプリはすべてのユーザーがWebSocketで接続可能になっている。
コネクションを利用するとWebSockets接続の認証許可処理を行うことができる。
Userモデルを作成し、ユーザーがログイン済みの時にCookieへのユーザーIDが設定されている状態を想定。
仮のログイン機能を用意する為に。Userモデルを作成。
rails g model User name
rails db:migrate
rails console
irb> User.create(name: 'チャット太郎')
CookieへユーザーIDを保存する処理をコントローラーへ追加する。
実際のユーザー認証をかくと多くのコードが必要になるのでここでは決め打ちの処理を追加。
- app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
def show
+ cookies.signed[:user_id] = User.first.id
@messages= Message.all
end
end
CookieへユーザーIDが保存されている仮のログイン機能ができたので
これを元にActionCable側で認証処理を追加
- app/channels/application_cable/channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
identified_by :current_user
# 接続ごとに一意の識別子を設定 current_userを識別子として使用。
# current_userは接続ごとにユニークなユーザーオブジェクトを格納します。
# この識別子を使用して、接続されたユーザーを特定できる
def connect
# クライアントがWebSocket接続を確立したときに呼び出されます。
self.current_user = find_verified_user
# find_verified_userメソッドを呼び出して、認証されたユーザーを代入
end
private
def find_verified_user
if verified_user = User.find_by(id: cookies.signed[:user_id])
# cookieに格納されたuser_idを使用して、ユーザーをデータベースから検索
# cookies.signed[:user_id]は、署名付きクッキーからユーザーIDを取得します。
# 署名付きクッキーは、改ざん防止のために署名されており、安全に使用できます
verified_user
else
reject_unauthorized_connection
# 認証されていない接続を拒否します。認証されていないユーザーはWebSocket接続を確立できません
end
end
end
end
- ActionCableをサブドメインで運用している場合は両方のドメインでcookieを共有できるように設定する必要がある。
- identified_byメソッドに引数:current_user渡すとcurrent_userおよび
**current_user=**メソッドが作成される。
[Action Cable の概要 - Railsガイド](https://railsguides.jp/action_cable_overview.html)
WebSoket経由で受け取ったメッセージはActionCable用のワーカースレッドで処理されます。
デフォルトでは1プロセスにつき最大4つのスレッドが使われる設定になっている
-
スレッド:
- スレッドは、プロセス内で実行される軽量な実行単位です。スレッドを使うと、同じプログラム内で複数のタスクを同時に実行できます。
-
ワーカースレッド:
- ワーカースレッドは、特定のタスク(ここではWebSocket経由で受け取ったメッセージの処理)を実行するために使用されるスレッドです。
- ActionCableは、WebSocket経由で受け取ったメッセージを処理するためにワーカースレッドを使用します。
- メッセージがサーバーに届くと、それを処理するためにワーカースレッドが割り当てられます。
- ワーカースレッドは、並行して複数のメッセージを処理することで、リアルタイム通信のパフォーマンスを向上させます。
- デフォルトでは、1つのActionCableプロセスあたり最大4つのワーカースレッドが使用されるように設定されています。
- これは、プロセスが同時に4つのメッセージを並行して処理できることを意味します。
-
シナリオ:
- ユーザーA、ユーザーB、ユーザーC、ユーザーD、ユーザーEの5人がチャットルームに参加しているとします。
- それぞれが同時にメッセージを送信します。
-
メッセージの受信と処理:
- これらのメッセージはWebSocket経由でサーバーに届きます。
- サーバー側のActionCableがこれらのメッセージを受け取ります。
-
ワーカースレッドによる処理:
- サーバーには4つのワーカースレッドがあるため、最初の4つのメッセージ(ユーザーA、B、C、Dのメッセージ)はそれぞれのスレッドによって同時に処理されます。
- 5番目のメッセージ(ユーザーEのメッセージ)は、最初の4つのメッセージのいずれかが処理を終えるまで待機します。
スレッドの最大数を変更するにはproduction.rbに記述すれば良い
- config/environments/production.rb
config.action_cable.worker_pool_size = 10
config/database.ymlのpoolの数値も変更する。/cableにマウントされている同一プロセスでWebとWebsocketjを提供している時はActionCableのスレッド数とWeb用のスレッド数を足し合わせたもの、or それ以上にする。
WebSocket用のサーバーをWeb用と分離しスタンドアローンで起動している時はそれぞれスレッド数と同じ値、もしくはそれ以上にする
1. 同じプロセスでWebとWebSocketを提供している場合
- 同じプロセス: WebサーバーとWebSocketサーバーが同じプロセスで動作している場合、つまり、同じサーバーが両方の役割を果たしている場合です。
-
ActionCableのスレッド数とWeb用のスレッド数を足し合わせたもの:
- 例えば、Webサーバーが10個のスレッドを使い、ActionCable(WebSocketサーバー)が10個のスレッドを使う場合、合計で20個のスレッドが必要です。
-
データベース接続プールのサイズ:
- データベース接続プールのサイズを20(10 + 10)に設定することで、同時に20個のデータベース接続を確保します。
- これにより、すべてのスレッドがデータベースにアクセスできるようになります。
2. WebSocketサーバーをWebサーバーと分離している場合
- スタンドアローンで起動: WebサーバーとWebSocketサーバーが別々のプロセス(またはサーバー)として動作している場合です。これにより、WebサーバーとWebSocketサーバーが異なるリソースを使って動作します。
-
それぞれのスレッド数と同じ値、もしくはそれ以上:
- Webサーバーが10個のスレッドを持ち、WebSocketサーバー(ActionCable)が10個のスレッドを持つ場合、各プロセスでデータベース接続プールのサイズを10以上に設定します。
[WebSocketについて調べてみた。 - Qiita](https://qiita.com/south37/items/6f92d4268fe676347160)