koheitakahashiのブログ

2020.07.01にプログラマーとして生を受けた私が学んだことや、日常について徒然に書いていきます。

ActionCableの概要をまとめてみました

はじめに

少し前の記事で、自分が担当したActionCableを用いた機能実装について解説しました。

docs.koheitakahashi.com

上記の記事を書いているうちに、実装を担当することになった当初の気持ちが蘇ってきました。

  • 「Railsガイド読んでも全然分からない…」
  • 「WebSocketの仕様書を読んでも全然分からない…」
  • 「DHHのデモを見ても、コードを見ても全然分からない…」

と、「ナニモワカラナイ」という状態でした。

そこで、上記のような「ActionCableナニモワカラナイ」という方に向けて自分なりにActionCableの概要をまとめました。
ActionCableを理解するのに少しでも役に立てば幸いです。

また、理解や記述が間違っているところなどがあれば教えていただけるととても有り難いです。

そもそもActionCableとは

RailsでWebSocket通信を実現するためのフレームワークのことです。
RailsとJavaScriptの垣根をあまり感じることなく処理を記述できるのが素晴らしいところだと思っています。

具体的には、Rails側(channel.rb)で定義したメソッドをJavaScript側で呼び出すことができます(CRUDやbroadcastの処理など)。

WebSocketとは

生まれた背景

WebSocketとは、双方向通信を実現するために開発された通信規格です。

HTTPプロトコルで、双方向通信を行うためには一度確立したHTTPコネクションを開いたままにして、定期的にリクエストを送るということが必要でした。

しかし、その方法は本来想定されているHTTPプロトコルの使い方とは異なる、いわゆる裏技的なものでした。

そのため、双方向通信を行うためのプロトコルが開発されたという流れのようです。
HTTP通信で双方向通信を行おうとするよりも低コストだと言われています。

WebSocketコネクション確立までの流れ

WebSocketによる通信を行う前に一度HTTP通信を行い、それが正常に処理されてからWebSocket通信に移行します。このHTTP通信をハンドシェイクと呼びます。

もう少し具体的にハンドシェイクを見ていきます。
まず、ブラウザから、サーバーに以下のようなリクエストを送ります。

GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: http://example.com
Sec-WebSocket-Protocol: chat, superchat
Sec-WebSocket-Version: 13

(https://triple-underscore.github.io/RFC6455-ja.html より引用)

そして、サーバーは以下のようなレスポンスを返します。
ここで、ステータスコードが101以外なら、WebSocketに移行しません。

HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Sec-WebSocket-Protocol: chat

上記のハンドシェイクが正常に処理されたら、以降のデータのやりとりはWebSocketプロトコル上で行われるという流れになります。

以下はハンドシェイクのイメージ図です。 Image from Gyazo

ActionCableが実現しているWebSocket通信の世界観

ハンドシェイクが正常に処理されて、WebSocketプロトコルに移行した後のデータのやりとりを見ていきます。

ActionCable は pub/sub モデルというメッセージ送受信のモデルを採用しており、そのモデルに則ってデータがやりとりされる形になります。 pub/sub モデルの主要な概念は以下の3つです。

  • サーバーから送られるデータ
  • クライアントであるconsumer
  • 実際にデータがやりとりされる空間であるchannel

Image from Gyazo

ここで、consumerがchannelと繋がることをsubscribeといいます。
subscribeしたら、consumerはsubscriberと呼ばれることとなります。

このsubscriberに、サーバーがデータを送ることをbroadcastと呼びます(正確にはpubsubリンクと呼ばれるものを送っているようですが、私の調査能力ではその詳細を突き止めることができませんでした)。

Image from Gyazo

このようにconsumerがchannelをsubscribeして、そのsubscriberにデータがbroadcastされることで、データがクライアントに送られるという流れになります。

具体的にどのように書くのか

上記にデータのやりとりの大まかな流れを説明しました。
ここからは、上記のようなことをどのようにコードで書いていけば良いのかを説明します。

以下に紹介しているコードの例はRailsガイドや前回の私の記事を参考に記述しています。
そのまま書いて動くコードになるわけではないのでご了承ください。

ハンドシェイクを送る

まず、クライアント側でconsumerを設定する必要があります。

rails newした後のプロジェクトには、app/javascript/channels/consumer.jsに以下のように記述されていると思います。
consumerを特に設定する必要がない場合はここに記述を追加する必要はありません。

// app/javascript/channels/consumer.js
// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()

その後、アプリケーションの好きな場所で以下のように記述することでsubscriptionを確立しようとします。
今回は、app/javascript/channels/timelines_channel.jsというファイルがあると仮定して、そこに処理を書くならというイメージで説明しています。

subscriptionを確立しようとすることで、初めてハンドシェイクが送られます。

// app/javascript/channels/timelines_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "TimelinesChannel" })

WebSocketコネクションを確立する

app/channels/application_cable/connection.rbに処理を記述することで、ハンドシェイクが送られた後にWebSocketコネクションを確立するのか・しないのかを判断させられます。

以下のRailsガイドの例だと、Cookieに保存されているuser_idがDBに保存されている場合はWebSocketコネクションを確立して、それ以外の時は確立しないようにしているのが分かります。

# 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.encrypted[:user_id])
          verified_user
        else
          reject_unauthorized_connection
        end
      end
  end
end

Railsガイド ActionCableの章 から引用

subscribeしたときの処理

connection.rbにより、WebSocketコネクションを確立するか否かの判断がされ、無事コネクションが確立された後、指定したchannelをsubscribeすることとなります。

ここで、channelの処理を担うapp/channels/timelines_channel.rbが存在と仮定して説明します。

以下のコードのように、subscribeメソッドを定義すると、TimelinesChannelがsubscribeされたときの処理を記述できます。

例えば、以下ではTimelinesChannelがsubscribeされると、過去の分報を10件broadcastすることになります。
また、このstream_fromはどのような意味かというと、timelines_channelのsubscriberにbroadcastしてデータが送れるように道筋を示しているようなイメージです。

Railsガイドによると、

ブロードキャストでパブリッシュするコンテンツをサブスクライバ側にルーティングする機能をチャネルに提供します。 https://railsguides.jp/action_cable_overview.html#%E3%82%B9%E3%83%88%E3%83%AA%E3%83%BC%E3%83%A0 より

ということを行っているようです。

# app/channels/timelines_channel.rb
class TimelinesChannel < ApplicationCable::Channel
  def subscribed
    stream_from "timelines_channel"
    ActionCable.server.broadcast "timelines_channel", timeline: Timeline.take(10) }
  end
end

broadcastする

上記でも触れましたが、以下のように記述することで任意のchannelのsubscriberにデータをbroadcastできます。

以下は、createしたtimelinetimelines_channelのsubscriberにbroadcastしています。

timeline = Timeline.create(title: "分報だよ")
ActionCable.server.broadcast "timelines_channel", timeline: timeline

broadcastされたデータを受け取る

最後にbroadcastされたデータはフロントエンド側で、以下のようにcreateしたchannelの中で受け取ることができます。

以下は、受け取ったdataをコンソールログに表示するという処理です。

// app/javascript/channels/timelines_channel.js
import consumer from "./consumer"
consumer.subscriptions.create({ channel: "TimelinesChannel" }, 
    received(data) {
        console.log(data)
      }
)

まとめ

上記の流れをまとめると以下のようになります。

  1. consumerを設定(JS側)
  2. subscriptionを確立(JS側)
  3. subscriptionが確立されようとしたときにハンドシェイクが送られる
  4. リクエストを受け取ってWebSocketコネクションを確立するかを判断(Rails側)
  5. WebSocketコネクションが確立されたら、subscriptionを作成
  6. subscribeされた時にどのような処理を行うかを記述(Rails側)
  7. データをbroadcastする処理を記述(Rails側)
  8. broadcastされたデータを受け取る処理を記述(JS側)

このように設定や処理を記述することにより、サーバー・ブラウザ間でWebSocket通信が行われます。

この記事を通して、ActionCableが実現しているWebSocket通信の概要が少しで伝わったなら幸いです。

また、自分は上記のように理解してこの記事を書きましたが、理解や記述が間違っているところなどがあれば教えていただけると大変嬉しいです。

参考にさせていただいた資料

追記

  • 2020.11.08 WebSocket と pub/sub モデルの概念を混同して説明していたため修正。