ActionCable Protocol Overview

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について説明した。 実装が仕様なので、もちろんこの記事でまとめた内容が正しい保証はないけれど、クライアントを実装する際の参考にはなるでしょう。

参考リンク

この記事を書くにあたり、以下を参照した。