Action Cableのしくみ
主にRailsガイドをなぞったもので、冗長なので後に修正したいです。
Action Cableは、フロントのWebSocketとバックエンドのRails周りをシームレスに統合する、フルスタックなフレームワーク。
WebSocketとは、Webにおいての双方向通信を、従来のHTTP等よりも低コストで行うための仕組み。
Pub/Subモデル(出版-購読型モデル)
非同期型メッセージパラダイムの1種で、疎結合とそれによるスケーラビリティが利点。
メッセージを送信する側(Publisher)が、特定のメッセージを受信する側(Subscriber)の抽象クラスに情報を送信する。このときPublisherは、個別の受信者を指定しない。
Action Cableは、サーバーと多数のクライアント間の通信にこのアプローチを採用している。
サーバーサイド(Rails側)コンポーネント
1. Connection
- サーバーでWebSocketを受け付けるたびに、Connectionオブジェクトがインスタンス化する。
- Connection自体は、アプリのロジックは扱わない。
- Connectionは
ApplicationCable::Connection
のインスタンスである。 - Connectionオブジェクトは、全てのChannel Subscriptionsの親となる(2-1)。
- Consumer: WebSocket Connectionのクライアント。
- 各ユーザーが開くブラウザタブ、ウィンドウ、デバイスごとに、ConsumerのConnectionのペアが1つづつ作成される。
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.encryped[:user_id])
verified_user
else
reject_unauthorized_connection
end
end
end
end
2. Channel
- サーバーサイドのコンポーネントでは、Channelの設定が必要。
- MVCのコントローラと似ている。
- Railsはデフォルトで、Channel間で共有されるロジックをカプセル化する
ApplicationCable::Channel
という親クラスを作成する。
2-1.親Channelの設定
module ApplicationCable
class Channel < ActionCable::Channel::Base
end
end
class ChatChannel < ApplicationCable::Channel
end
class AppearanceChannel < ApplicationCable::Channel
end
Consumer(WebSocket Connectionのクライアント)は、このようにしてこれらのChannelをSubscribeできるようになる。
2-2. Subscrition(Rails側での実装)
class ChatChannel < ApplicationCable::Channel
# ConsumerがこのChannelのSubscriberになると
# このコードが呼び出される。
def subscribed
end
end
- ConsumerはChannelをSubscribeし、Subscriberとして振る舞う。
- 生成されたメッセージは、ActionCable Consumerから送信されるIDに基づいて、これらのChannel Subscriber側にルーティングされる。
##クライアントサイド(JavacSript側)コンポーネント
1. Connection
1-1. ConsumerとのConnection
//= require action_cable
//= require_self
//= require_tree ./channels
(function() {
this.App || (this.App = {});
App.cable = ActionCable.createConsumer();
}).call(this);
/cable
にデフォルトで接続するConsumerが準備される。
1-2. Subscriber(JavasScript側での設定)
指定のChannelにSubscriptionを作成することで、ConsumerがSubscriberになる。
App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" }
App.cable.subscriptions.create{ channel: "AppearanceChannel" }
Consumerは、指定のChannelに対するSubscriberとして振る舞うことができ、回数の制限はない。
たとえば、Consumerはチャットルームを同時にいくつでもSubscribeできる。
App.cable.subscriptions.create { channel: "ChatChannel", room: "1st Room" }
App.cable.subscriptions.create { channel: "ChatChannel", room: "2nd Room" }
クライアント-サーバー間のやりとり
1. Stream
Streamは、Channelにルーティング機能を与える。
これにより、ChannelはPublishされたコンテンツ(Broadcast)をSubscriberにルーティングできる。
class ChatChannel < ApplicationCable::Cannel
def subscribed
stream_for "chat_#{params[:room]}"
end
end
モデルに関連するStreamがある場合、利用するBroadcastがそのモデルとChannelの両方から生成される。
class CommentsChannel < ApplicationCable::Channel
def subscribed
post = Post.find(params[:id])
stream_for post
end
end
これで、このChannelに次のようにBroadcastできる。
CommentsChannel.broadcast_to(@post, @comment)
2. Broadcast
- Broadcastは、Pub/Subのリンク。
- Publisherからの送信内容はすべてBroadcastを経由する。
- Broadcastは、そのPublisherのBroadcastをStreamingしているChannelの、Subscriberに直接ルーティングされる。
- 各Channelは、0個以上のBroadcastをStreamingできる。
- Streamingを行っていないConsumerは、後で接続するときにBroadcastを取得できない。
3. Subscription
ChannelをSubscribeしたConsumerは、Subscriberとして振る舞う。
# Web通知の送信権をサーバーからリクエスト済みであることが前提
App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" },
received: (data) ->
@appendLine(data)
appendLine: (data) ->
html = @createLine(data)
$("[data-chat-room='Best Room']").append(html)
createLine: (data) ->
```
<article class="chat-line">
<span class="speaker">#{data["sent_by"])</span>
<span class="body">#{data["body"])</span>
</article>
```
4. Channelにパラメータを渡す
Subscription作成時に、クライアント側のパラメータをサーバー側に渡すことができる。
class ChatChannel < Application::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
end
subscriptions.create
に最初の引数として渡されるオブジェクトは、Action Cableチャンネルのparamsハッシュになります。キーワードのchannelの指定は省略できません。
App.cable.subscriptions.create { channel: "ChatChannel", room: "Best Room" },
received: (data) ->
@appendLine(data)
appendLine: (data) ->
html = @createLine(data)
$("[data-chat-room='Best Room']").append(html)
createLine: (data) ->
```
<article class="chat-line">
<span class="speaker">#{data["sent_by"])</span>
<span class="body">#{data["body"])</span>
</article>
```
# アプリ内のどこかで呼び出されるコード
ActionCable.server.broadcast(
"chat_#{room}",
sent_by: 'Paul',
body: 'This is a cool chat app.'
)
5. メッセージを再Broadcastする
あるクライアントから、接続している別のクライアントに、メッセージを再Broadcastすることはよくある。
class ChatChannel < ApplicationCable::Channel
def subscribed
stream_from "chat_#{params[:room]}"
end
def receive(data)
ActionCable.server.broadcast("chat_#{params[:room]}", data)
end
end
App.chatChannel = App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" },
received: (data) ->
# data => { sent_by: "Paul", body: "This is a cool chat app." }
App.chatChannel.send({ sent_by: "Paul", body: "This is a cool chat app." })
再Broadcastは、接続しているすべてのクライアントで受信される。
送信元クライアント自身も再Broadcastを受信する。
利用するparamsは、ChannelをSubscribeするときと同じ。
フルスタック(なコンポーネント)の例
###設定手順
- Connectionを設定
- 親Channelを設定
- Consumerを接続(Subscription)
例1
ユーザーがオンラインかどうか、ユーザーがどのページを開いているかという情報を追跡するChannelの簡単な例。
# サーバー側
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
Subscriptionが開始されると、subscribedコールバックがトリガーされ、そのユーザーがオンラインであることが示される。
このアピアランスAPIをRedisやデータベースなどと連携することもできる。
# クライアント側
App.cable.subscriptions.create "AppearanceChannel",
# Subscriptionがサーバー側で利用可能になると呼び出される。
connected: ->
@install()
@appear()
# WebSocket Connectionが閉じると呼び出される。
disconnected: ->
@uninstall()
# Subscriptionがサーバーに拒否されると呼び出される。
rejected: ->
@uninstall()
appear: ->
# サーバーの`AppearanceChannel#appear(data)`を呼び出す
@perform("appear", appearing_on: $("main").data("appearing-on"))
away: ->
# サーバーの`AppearanceChannel#away`を呼び出す
@perform("away")
buttonSelector = "[data-behavior~=appear_away]"
install: ->
$(document).on "turbolinsk:load.appearnace", =>
@appear()
$(document).on "click.appearnace", buttonSelector, =>
@away()
false
$(buttonSelector).show()
uninstall: ->
$(document).off(".appearance")
$(buttonSelector).hide()
-
クライアントはサーバーに
App.cable = ActionCable.createConsumer("ws://cable.example.com")
経由で接続する(cable.js)。サーバーは、このConnectionの認識にcurrent_user
を使う。 -
クライアントはアピアランスChannelに
App.cable.subscriptions.create(channel: "AppearanceChannel")
経由で接続する(appearnace.coffee) -
サーバーは、アピアランスChannel向けに新しいSubscriptionを開始したことを認識し、サーバーの
subscribed
コールバックを呼び出し、current_user
のappear
メソッドを呼び出す。 -
クライアントは、Subscriptionが確立したことを認識し、
connected
(appearance.coffee)を呼び出す。これにより、@install
と@appear
が呼び出される。@appear
はサーバーのAppearanceChannel#appear(data)
を呼び出して{ appearing_on: $("main").data("appearing-on") }
のデータハッシュを渡す。なお、この動作が可能なのは、クラスで宣言されている(コールバックを除く)全パブリックメソッドが、サーバー側のChannelインスタンスから自動的に公開されるからです。公開されたパブリックメソッドは、Subscriptionでperform
メソッドを使って、RPC(リモートプロシージャコール)として利用できる。 -
サーバーは、
current_user
で認識したConnectionのアピアランスChannelで、appear
アクションへのリクエストを受信する。(appearance_channel.rb) サーバーは:appearing_on
キーを使ってデータをデータハッシュから取り出し、current_user.appear
に渡される:on
キーの値として設定する。
例2 新たなWebノーティフィケーションを受け取る
アピアランスの例では、WebSocket Connectionを使って、サーバーからクライアントを呼び出していた。
WebSocketでは双方向通信を利用できるので、この例では、サーバーがクライアントのアクションを呼び出してみる。
このWeb Notificfation Channelは、正しいStreamにBroadcastを行ったときに、クライアント側でWeb通知を表示する。
# サーバーサイド
class WebNotificationsChannel < ApplicationCable::Channel
def subscribed
stream_for current_user
end
end
# クライアントサイド
# サーバーからWebノーティフィケーションの送信権を
# リクエスト済みであることが前提
App.cable.subscriptions.create "WebNotificationsChannel",
received: (data) ->
new Notification data["title"], body: data["body"]
アプリケーションのどこからでも、Web Notification ChannelのインスタンスにコンテンツをBroadcastできる。
# このコードはアプリケーションのどこか(NewCommentJobあたり)で呼び出される
WebNotificationsChannel.broadcast_to(
current_user,
title: 'New things!',
body: 'All the news fit to print'
)
WebNotificationsChannel.broadcast_to
呼び出しでは、ユーザーごとに異なるBroadcast名が使われるが使われている下で、現在のSubscriptionアダプタのpubsubキューにメッセージを設定する。
IDが1のユーザーなら、ブロードキャスト名はweb_notifications:1
のようになる。
このChannelは、web_notifications:1
に着信するものすべてを、received
コールバック呼び出しによってクライアントに直接Streamingするようになる。
引数として渡されたデータは、サーバー側のBroadcast呼び出しに2番目のパラメータとして渡されたハッシュ。このハッシュはJSONでエンコードされ、receivedとして受信したデータ引数から取り出される。
用語まとめ
-
サーバーサイド(.rb)
- Connection
- Channel(コントローラ)
-
クライアントサイド(.js, .coffee)
- Consumer
- Subscriber
-
Stream(ルーティング)
- Broadcastを経由