はじめに
少し前の記事で、自分が担当したActionCableを用いた機能実装について解説しました。
上記の記事を書いているうちに、実装を担当することになった当初の気持ちが蘇ってきました。
- 「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プロトコル上で行われるという流れになります。
ActionCableが実現しているWebSocket通信の世界観
ハンドシェイクが正常に処理されて、WebSocketプロトコルに移行した後のデータのやりとりを見ていきます。
ActionCable は pub/sub モデルというメッセージ送受信のモデルを採用しており、そのモデルに則ってデータがやりとりされる形になります。 pub/sub モデルの主要な概念は以下の3つです。
- サーバーから送られるデータ
- クライアントであるconsumer
- 実際にデータがやりとりされる空間であるchannel
ここで、consumerがchannelと繋がることをsubscribeといいます。
subscribeしたら、consumerはsubscriberと呼ばれることとなります。
このsubscriberに、サーバーがデータを送ることをbroadcastと呼びます(正確にはpubsubリンクと呼ばれるものを送っているようですが、私の調査能力ではその詳細を突き止めることができませんでした)。
このように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
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したtimeline
をtimelines_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) } )
まとめ
上記の流れをまとめると以下のようになります。
- consumerを設定(JS側)
- subscriptionを確立(JS側)
- subscriptionが確立されようとしたときにハンドシェイクが送られる
- リクエストを受け取ってWebSocketコネクションを確立するかを判断(Rails側)
- WebSocketコネクションが確立されたら、subscriptionを作成
- subscribeされた時にどのような処理を行うかを記述(Rails側)
- データをbroadcastする処理を記述(Rails側)
- broadcastされたデータを受け取る処理を記述(JS側)
このように設定や処理を記述することにより、サーバー・ブラウザ間でWebSocket通信が行われます。
この記事を通して、ActionCableが実現しているWebSocket通信の概要が少しで伝わったなら幸いです。
また、自分は上記のように理解してこの記事を書きましたが、理解や記述が間違っているところなどがあれば教えていただけると大変嬉しいです。
参考にさせていただいた資料
追記
- 2020.11.08 WebSocket と pub/sub モデルの概念を混同して説明していたため修正。