ActionCableは実装が仕様とはいえ、ActionCableのクライアントを書く際に困ったのでまとめてみる。 正確性は保証しない。 また、この記事は2017年11月15日時点での情報を元にしている。
そもそもActionCableって何
Rails5で登場した、websocketを介し、railsとフロントエンドをイベントドリブンな形式で繋ぐための実装。 技術的にはwebsocketのTextFrameのみを利用し、pingなどの各メッセージを送受信したりしてPub/Subを実現するもの。
用語定義
- Channel
- 一連のイベントを論理的にまとめるもの。 イベントは何らかのChannelに紐付けて発行される。
- Publisher
- Channelに対してイベントを発行するもの。
- Consumer
- Channelに発行されるイベントを受信するもの。
- Subscription
- Channelと紐付いた、Channel固有のイベントを扱うもの。
Protocol Overview
実装から読み取ったActionCableのプロトコル(アプリケーションレイヤ)について記載する。
Message Types
ConsumerとPublisher間では、JSONによりデータが送受信される。 ここではそれらのJSONのスキーマを、TypeScriptのインタフェイス定義の形式で記載する。
// Channelを定義するためのデータ // アプリケーションの定義により、任意のデータを内包することができる。 // "channel"というキーは予約済の扱い。 export interface Channel { channel: string; [name: string]: any; } // どのChannel、Subscriptionに対するCommand/Messageであるかを識別するためのデータ // JSON.stringify(Channel)した文字列が入る。 type Identifier = string; // ConsumerとPublisher間の物理的な接続が確立した際に、Publisherから送られてくる接続確立を示すデータ export interface WelcomeMessage { type: "welcome"; } // websocketのPingFrameをTextFrameによりエミュレートするためのデータ export interface PingMessage { type: "ping"; message: string; } // SubscribeCommandが受理されたことを示すデータ export interface ConfirmSubscriptionMessage { type: "confirm_subscription"; identifier: Identifier; } // SubscribeCommandが拒否されたことを示すデータ export interface RejectSubscriptionMessage { type: "reject_subscription"; identifier: Identifier; } // Channel固有のデータ // アプリケーションにより定義される。 export interface ChannelMessage { identifier: Identifier; message: any; } // ConsumerがPublisherから受信するデータ export type Message = WelcomeMessage | PingMessage | ConfirmSubscriptionMessage | RejectSubscriptionMessage | ChannelMessage; // Channelに対して、ConsumerとPublisher間でデータの送受信開始を要求するデータ export interface SubscribeCommand { command: "subscribe"; identifier: Identifier; } // Channelに対して、ConsumerとPublisher間でデータの送受信停止を要求するデータ export interface UnsubscribeCommand { command: "unsubscribe"; identifier: Identifier; } // Channel固有のデータ // アプリケーションにより定義される。 export interface ChannelCommand { command: "message"; identifier: Identifier; data: string; } // ConsumerがPublisherに対して送信するデータ export type Command = SubscribeCommand | UnsubscribeCommand | ChannelCommand;
データフロー
ここではMessageTypesで定義したインタフェイス名を用いて、websocket上のデータフローについて記載する。
ConsumerがPublisherに対して接続を開始し、Subscriptionを確立するまでのフローは以下となる。
@startuml activate Consumer activate Publisher ' Publisherとの物理コネクション確立 Consumer -> Publisher : (websocketによる接続) Consumer <<-- Publisher : WelcomeMessage ' Subscription確立 Consumer -> Publisher : SubscribeCommand Consumer <<-- Publisher : ConfirmSubscriptionMessage Consumer -> Consumer : Subscriptionの確立 @enduml
Subscription確立後は、ChannelCommandやChannelMessageの送受信が開始されるが、アプリケーション定義の部分となるためここでは記載しない。 また、PingMessageは上記データフローとは独立しており、ConsumerはPublisherから一方的に受信し続ける。
ルーティング
ActionCableは、MessageTypesで定義したMessageをConsumerが受け取った際に、適切なSubscriptionに対して該当Messageをルーティングする。 ここでルーティングという言葉は、Messageの持つidentifierプロパティに従い、同一の文字列と関連するSubscriptionに該当Messageを渡して処理を行わせることを意味する。
擬似的なルーティング処理を、TypeScriptによるコード例として記載する。
class Subscriptions { subscriptions: Array<Subscription> = []; onMessage(message: ChannelMessage): void { this.subscriptions .filter(subscription => subscription.identifier === message.identifier) .forEach(subscription => subscription.onMessage(message.message)); } } interface Subscription { identifier: Identifier; onMessage(data: any): void; } const subscriptions = new Subscriptions(); const message: ChannelMessage = { identifier: "{\"channel\":\"ChatChannel\"}", message: { text: "hello" } }; subscriptions.onMessage(message);
自動再接続
ActionCableではwebsocketを実際のデータ転送に用いているが、ネットワークの一時的な切断などでwebsocket接続が切断されてしまうケースは往々にして存在する。 ActionCableには、websocket接続を自動的に復旧する仕組みが備わっている。
ただしこの仕組みは、単にwebsocket接続を監視し、切断を検知した際に自動的に再度websocket接続を行うだけのため、特に説明しない。
まとめ
ActionCableの実装から、そこで用いられるwebsocketのTextFrame上を流れるJSONについて説明した。 実装が仕様なので、もちろんこの記事でまとめた内容が正しい保証はないけれど、クライアントを実装する際の参考にはなるでしょう。
参考リンク
この記事を書くにあたり、以下を参照した。