93
86

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Rails】Action Cableについてまとめてみた

Last updated at Posted at 2019-05-04

Action Cableのしくみ

主にRailsガイドをなぞったもので、冗長なので後に修正したいです。

Action Cableは、フロントのWebSocketとバックエンドのRails周りをシームレスに統合する、フルスタックなフレームワーク。

WebSocketとは、Webにおいての双方向通信を、従来のHTTP等よりも低コストで行うための仕組み。

WebSocketについて調べてみた。

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つづつ作成される。
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
    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の設定

app/channels/application_cable_channel.rb
module ApplicationCable
  class Channel < ActionCable::Channel::Base
  end
end
app/channel/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end

Consumer(WebSocket Connectionのクライアント)は、このようにしてこれらのChannelをSubscribeできるようになる。

2-2. Subscrition(Rails側での実装)

app/channels/chat_channel.rb
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

app/assets/javascripts/cable.js
//= 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/assets/javascripts/cable/subscriiptions/chat.coffee

App.cable.subscriptions.create{ channel: "ChatChannel", room: "Best Room" }
app/assets/javascripts/cable/subscriiptions/appearance.coffee

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にルーティングできる。

app/channels/chat_channel.rb
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として振る舞う。

app/assets/javascripts/cable/subscriiptions/chat.coffee
# 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作成時に、クライアント側のパラメータをサーバー側に渡すことができる。

app/channels/chat_channel.rb
class ChatChannel < Application::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end

subscriptions.createに最初の引数として渡されるオブジェクトは、Action Cableチャンネルのparamsハッシュになります。キーワードのchannelの指定は省略できません。

app/assets/javascripts/cable/subscriptions/chat.coffee
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することはよくある。

app/channels/chat_channel.rb
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/assets/javascripts/cable/subscriptions/chat.coffee
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するときと同じ。

フルスタック(なコンポーネント)の例

###設定手順

  1. Connectionを設定
  2. 親Channelを設定
  3. Consumerを接続(Subscription)

例1

ユーザーがオンラインかどうか、ユーザーがどのページを開いているかという情報を追跡するChannelの簡単な例。

app/channels/appearance_channel.rb
# サーバー側
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/assets/javascripts/cablesubscriptions/appearance.coffee
# クライアント側
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()
  1. クライアントはサーバーにApp.cable = ActionCable.createConsumer("ws://cable.example.com")経由で接続する(cable.js)。サーバーは、このConnectionの認識にcurrent_userを使う。
  2. クライアントはアピアランスChannelにApp.cable.subscriptions.create(channel: "AppearanceChannel")経由で接続する(appearnace.coffee)
  3. サーバーは、アピアランスChannel向けに新しいSubscriptionを開始したことを認識し、サーバーのsubscribedコールバックを呼び出し、current_userappearメソッドを呼び出す。
  4. クライアントは、Subscriptionが確立したことを認識し、connected(appearance.coffee)を呼び出す。これにより、@install@appearが呼び出される。@appearはサーバーのAppearanceChannel#appear(data)を呼び出して{ appearing_on: $("main").data("appearing-on") }のデータハッシュを渡す。なお、この動作が可能なのは、クラスで宣言されている(コールバックを除く)全パブリックメソッドが、サーバー側のChannelインスタンスから自動的に公開されるからです。公開されたパブリックメソッドは、Subscriptionでperformメソッドを使って、RPC(リモートプロシージャコール)として利用できる。
  5. サーバーは、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通知を表示する。

app/channels/web_notifications_channel.rb
# サーバーサイド
class WebNotificationsChannel < ApplicationCable::Channel
  def subscribed
    stream_for current_user
  end
end
app/assets/javascripts/cable/subscriptions/web_notifications.coffee
# クライアントサイド
# サーバーからWebノーティフィケーションの送信権を
# リクエスト済みであることが前提
App.cable.subscriptions.create "WebNotificationsChannel",
  received: (data) ->
    new Notification data["title"], body: data["body"]

アプリケーションのどこからでも、Web Notification ChannelのインスタンスにコンテンツをBroadcastできる。

.rb
# このコードはアプリケーションのどこか(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を経由

参考リンク

93
86
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
93
86

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?