第31章|今さら学ぶ「Action Cable(WebSocket)」
📚 シリーズ目次はこちら → 「今さら学ぶ」シリーズ — はじめに
🗺️ KnowledgeNoteの設計を確認 → 設計マップ
この章でわかること
- WebSocketとは — 電話のようにつなぎっぱなしの通信
- HTTPとの違い — 毎回かけ直す電話 vs つなぎっぱなしの電話
- Action Cableの仕組み — RailsでWebSocketを使うためのフレームワーク
- チャンネル・サブスクリプション・ブロードキャストの役割
- Solid Cable(Rails 8.0 標準)— Redis不要のWebSocket
- KnowledgeNoteでの例:リアルタイム通知
🏠 たとえ話で掴む「WebSocket」
通常のHTTP通信は 毎回かけ直す電話 です。「新しい通知ある?」→「ないよ」→ 切る。5秒後にまた「新しい通知ある?」→「ないよ」→ 切る。何もなくても電話をかけ続けるので非効率になります。
WebSocket は つなぎっぱなしの電話 です。一度つないだら切らずに、サーバ側から「新しい通知が来たよ!」と即座に伝えられます。
| HTTP | WebSocket |
|---|---|
| 毎回電話をかけ直す | つなぎっぱなし |
| クライアントから聞きに行く | サーバから教えてくれる |
| リクエスト→レスポンスの一方通行 | 双方向通信 |
| ページ表示・フォーム送信に適する | チャット・通知・リアルタイム更新に適する |
WebSocketとは何か — 技術的な定義
HTTPは ステートレス なプロトコルです(→ 第6章)。クライアントがリクエストを送り、サーバがレスポンスを返したら、その接続は終わります。サーバから能動的にデータを送る手段がありません。
これでは「新しいコメントがついた瞬間にページを更新する」「チャットメッセージをリアルタイムで表示する」といったことができません。クライアントが定期的にサーバに問い合わせる ポーリング という方法もありますが、変化がないときも問い合わせ続けるので無駄が多くなります。
WebSocket は、この問題を解決するための通信プロトコルです。最初のHTTPリクエスト( ハンドシェイク)で「これからWebSocketで話そう」と合意した後、HTTPの接続を WebSocket接続にアップグレード します。以降は接続が維持され、サーバとクライアントの 双方向通信 が可能になります。
【HTTPのポーリング(従来のやり方)】
クライアント → サーバ 「新着ある?」 → 「ない」
クライアント → サーバ 「新着ある?」 → 「ない」
クライアント → サーバ 「新着ある?」 → 「1件あるよ」
(変化がなくても毎回リクエストが飛ぶ)
【WebSocket】
クライアント → サーバ 「WebSocketで接続したい」(ハンドシェイク)
クライアント ⇄ サーバ (接続が維持される)
サーバ → クライアント 「新着が来たよ!」(必要なときだけ送信)
URLスキームの違い
WebSocketの接続先URLは、HTTPとは異なるスキームを使います。
| プロトコル | スキーム | 用途 |
|---|---|---|
| HTTP |
http:// / https://
|
通常のWebページ |
| WebSocket |
ws:// / wss://
|
WebSocket通信(sはSSL暗号化) |
いつWebSocketを使うか
すべての通信をWebSocketにする必要はありません。使い分けの判断基準は「サーバからのプッシュが必要かどうか」です。
| 方式 | 仕組み | 適する場面 |
|---|---|---|
| 通常のHTTP | クライアントがリクエスト→レスポンス | ページ表示、フォーム送信、API呼び出し |
| ポーリング | 一定間隔でHTTPリクエストを繰り返す | 更新頻度が低い場合(数分〜数十分に1回) |
| WebSocket | 接続を維持して双方向通信 | チャット、通知、リアルタイム共同編集 |
KnowledgeNoteのようなアプリでは、記事の閲覧やフォーム送信は通常のHTTPで十分です。リアルタイム通知の部分だけWebSocketを使います。
📡 Action Cable — RailsのWebSocketフレームワーク
Action Cable は、RailsでWebSocketを扱うための標準フレームワークです。WebSocketの接続管理・認証・メッセージの振り分けを、Railsの作法(チャンネルという概念)に沿って実装できます。
基本概念
サーバ(放送局) クライアント(視聴者)
┌──────────────┐ ┌──────────────────┐
│ Connection │ │ │
│ (認証・接続管理) │ │ │
│ │ │ │
│ Channel │ WebSocket │ subscription │
│ (通知チャンネル) │ ◄──────────► │ (チャンネル登録) │
│ │ 双方向通信 │ │
└──────────────┘ └──────────────────┘
| 用語 | 意味 | たとえ |
|---|---|---|
| Connection | WebSocket接続の入り口。認証を担当 | 放送局の受付(会員証を確認) |
| Channel | サーバ側の通信窓口。用途ごとに分ける | テレビのチャンネル(NHK、日テレ等) |
| Subscription | クライアントがチャンネルに接続すること | チャンネル登録 |
| Broadcast | サーバから登録者全員にデータを送ること | 放送 |
| Stream | 誰にどのデータを送るかの振り分け | 「user_1 宛の通知」のような個別配信 |
Connection — 接続時の認証
WebSocket接続が確立されるとき、最初に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
# Devise の Warden からログイン中のユーザーを取得する
def find_verified_user
env["warden"]&.user || reject_unauthorized_connection
# → Warden(Devise)が認証情報を保持しているため、セッション済みのユーザーをそのまま取得できる
# → 認証できなければ接続を拒否
end
end
end
identified_by :current_user を宣言すると、以降のチャンネル内で current_user が使えるようになります。HTTPのコントローラでいう current_user と同じ役割です。
💡 認証方式による違い
- Devise を使っている場合 (KnowledgeNote):
env["warden"]&.userでログイン済みユーザーを取得できます。- Rails 8 組み込み認証を使っている場合:
Session.find_by(id: cookies.signed[:session_token])&.userのように、セッションテーブル経由で取得します(rails generate authenticationで生成されるSessionモデルを使用)。cookies.encrypted[:user_id]はどちらの方式にも標準では対応していないため、Auth方式に合わせた方法を選んでください。
🛠️ KnowledgeNoteでの具体例:リアルタイム通知
「田中さんが記事を投稿したら、フォロワーの鈴木さんの画面に即座に通知バッジが表示される」——この流れをAction Cableで実装します。
1. チャンネルの作成
# app/channels/notification_channel.rb
class NotificationChannel < ApplicationCable::Channel
def subscribed
# ログインユーザー専用のストリームに接続
# → current_user は Connection で設定したもの
stream_for current_user
end
def unsubscribed
# 接続が切れたときのクリーンアップ処理(必要に応じて)
end
end
stream_for current_user は「このユーザー専用の配信チャンネル」を作ります。田中さん宛の通知は田中さんにだけ、鈴木さん宛は鈴木さんにだけ届きます。
2. サーバからブロードキャスト
# app/jobs/notification_job.rb
# 記事の公開時にフォロワーへ通知を送るジョブ(→ [第29章](https://qiita.com/harapeco-mgn/items/420a7d80595504b0444d))
class NotificationJob < ApplicationJob
queue_as :default
def perform(article)
article.user.followers.find_each do |follower|
# 通知レコードを作成
notification = Notification.create!(
user: follower,
notifiable: article,
action_type: "published"
)
# WebSocket経由でリアルタイム送信
NotificationChannel.broadcast_to(
follower, # 送信先ユーザー(stream_for の対象)
{
count: follower.notifications.unread.count,
# ↑ unread スコープは[第16章](https://qiita.com/harapeco-mgn/items/49f2667692e4e109a0c5)で定義済み
message: "#{article.user.name}さんが新しい記事を投稿しました"
}
)
end
end
end
# コントローラからジョブを呼び出す
# app/controllers/articles_controller.rb
def publish
@article.published!
NotificationJob.perform_later(@article)
# → ジョブはバックグラウンドで実行される(→ [第29章](https://qiita.com/harapeco-mgn/items/420a7d80595504b0444d))
redirect_to @article, notice: t("articles.publish.success")
end
3. クライアント側(JavaScript)
// app/javascript/channels/notification_channel.js
import consumer from "channels/consumer"
// NotificationChannel に接続(サブスクリプション)
consumer.subscriptions.create("NotificationChannel", {
// サーバからデータが届いたときのコールバック
received(data) {
const badge = document.querySelector("[data-notification-badge]")
if (badge) {
badge.textContent = data.count
badge.classList.remove("hidden")
}
}
})
<%# app/views/layouts/_header.html.erb %>
<%# 通知バッジ(未読数を表示) %>
<span
data-notification-badge
class="<%= 'hidden' if current_user.notifications.unread.count.zero? %>"
>
<%= current_user.notifications.unread.count %>
</span>
4. 動作の全体フロー
① 田中さんが記事を公開する
↓
② コントローラが NotificationJob.perform_later を呼ぶ
↓
③ ジョブがバックグラウンドで実行(Solid Queue → [第29章](https://qiita.com/harapeco-mgn/items/420a7d80595504b0444d))
↓
④ フォロワー(鈴木さん)に Notification レコードを作成
↓
⑤ NotificationChannel.broadcast_to で鈴木さん宛にJSONを送信
↓
⑥ 鈴木さんのブラウザが WebSocket 経由で受信
↓
⑦ received(data) が実行され、通知バッジの数字がリアルタイムで更新(🔔 3)
🔌 Solid Cable(Rails 8.0 標準)
Action Cableは、WebSocketのメッセージを中継するために Pub/Sub(パブリッシュ/サブスクライブ)バックエンド を必要とします。
従来はこのバックエンドに Redis を使うのが一般的でした。Redisはインメモリのデータストアで高速ですが、別途サーバの用意と管理が必要でした。
Rails 8.0 では Solid Cable がデフォルトのバックエンドです。Pub/SubをDBで管理するため、Redisサーバが不要になります。
# config/cable.yml
# 開発環境
development:
adapter: async
# → 開発中はサーバプロセス内で完結する簡易アダプタ
# 本番環境
production:
adapter: solid_cable
# → Redis不要。DBテーブルでPub/Subを管理
silence_polling: true
# → ポーリングのログを抑制
polling_interval: 0.1.seconds
# → メッセージの確認間隔
従来のRedis方式との比較
| 従来(Redis) | Solid Cable(Rails 8.0) | |
|---|---|---|
| 追加ミドルウェア | Redis サーバが必要 | 不要(DBだけで動く) |
| 設定 |
redis gem + Redis接続設定 |
デフォルトで有効 |
| パフォーマンス | 非常に高速(インメモリ) | DBアクセスが入るぶん若干遅い |
| 適するアプリ | 大規模チャットアプリ(数千同時接続) | 通知・軽いリアルタイム更新 |
| インフラコスト | Redis サーバの費用が追加 | DB のみで済む |
KnowledgeNoteのような「通知バッジのリアルタイム更新」程度であれば、Solid Cableで十分です。秒間数千メッセージを処理するチャットアプリのような場合は、Redis方式を検討します。
💡 Rails 7 との違い
Rails 7 まではAction CableのデフォルトバックエンドがRedisでした。
Rails 8.0 ではrails newした時点で Solid Cable が設定されており、
Redisのインストールなしに WebSocket が動作します。
💼 面接で聞かれたら?
Q:WebSocketとHTTPの違いを説明してください。
「HTTPはクライアントがリクエストを送って初めてサーバが応答する一方通行の通信です。WebSocketは最初のHTTPハンドシェイクの後に接続をアップグレードして維持し、サーバからクライアントに任意のタイミングでデータを送れる双方向通信です。リアルタイム通知やチャット機能に適しています。RailsではAction Cableで実装し、Rails 8.0ではSolid CableによりRedis不要で利用できます。」
深掘りされたら:
- 「Action Cableの構成要素は?」→ Connection(認証)、Channel(通信窓口)、Subscription(クライアントの接続)の3層構造。Connectionで
current_userを特定し、Channelでstream_forを使って誰にどのデータを送るかを振り分ける。- 「ポーリングとの違いは?」→ ポーリングはクライアントが一定間隔でHTTPリクエストを繰り返す方式。変化がなくてもリクエストが飛ぶので無駄が多い。WebSocketは接続を維持して必要なときだけデータを送るので効率的。
- 「Solid Cableとは?」→ Rails 8.0標準のAction Cableバックエンド。従来はRedisが必要だったPub/Sub機能をDBで実現し、追加のミドルウェアなしでWebSocketが動く。通知レベルの用途には十分な性能。
🔗 もっと深く知りたい人へ(1次情報リンク)
- Rails ガイド:Action Cable の概要 — チャンネル・ストリーム・ブロードキャストの全体像
- Solid Cable(GitHub) — Rails 8.0 標準のWebSocketアダプタ
- MDN Web Docs:WebSocket API — WebSocketプロトコルの仕様
- RFC 6455: The WebSocket Protocol — WebSocketの原文仕様(英語)
まとめ
- ✅ WebSocketは「つなぎっぱなしの電話」。HTTPのハンドシェイク後に接続をアップグレードし、双方向通信を実現する
- ✅ Action CableはRails標準のWebSocketフレームワーク。Connection(認証)→ Channel(通信窓口)→ Stream(振り分け)の3層構造
- ✅
stream_for current_userで特定ユーザー宛の配信チャンネルを作り、broadcast_toでデータを送信 - ✅ クライアント側は
consumer.subscriptions.createでチャンネルに接続し、received(data)でデータを受け取る - ✅ Rails 8.0のSolid CableでRedis不要。通知レベルの用途にはDBベースのPub/Subで十分
📚 シリーズ目次:「今さら学ぶ」シリーズ — はじめに