【アーキテクチャ】

はじめに

ここではシステム特有の用語を含めたメカニズムや考え方を説明しています。

このシステムではノンブロッキングモードによる実装を前提としており、TCPやUDP通信を使った接続中の個別の挙動部分については、待ちを発生させないタイムシェアリングによる並行動作を可能にしているため、動的なプロセスのフォークやスレッドの生成を行わなくても実装できるようにしています。
こうする事でプロセスの絶対数を管理する事ができ、サーバー間通信を伴うマルチサーバーの構築も可能なので、サーバーリソースを極力抑えつつパフォーマンスを向上させ、スケーラビリティを考慮しながら高速に動作できるように設計しています。

プロセスの絶対数を管理する部分の考え方については、FastCGIを使ったAPサーバーのプロセス管理手法と一部似通っています。

レイヤー概念図

サーバーのアプリケーションの部分はプロトコル部とコマンド部に大きく分かれています。
最初にそれらをライブラリ内のCycleDrivenManagerのプロトコル部とコマンド部へ取り込ませてから使う事になります。

プロトコル部とコマンド部に分けているのは、それぞれを自由に入れ替える事ができるようにするためです。
作成したクライアントのプロトコルに合わせてプロトコル部を入れ替えたり、サーバーサイドのコンテンツを切り替えるためにコマンド部を入れ替えたりする事で様々な組み合わせのサーバーを構築する事が可能です。

キューとUNITの関係

まずはCycleDrivenManagerの動きを理解するためにキューとステータスUNITの関係を知る必要があります。
図にすると以下のようなイメージになります。
通信データの送受信の特徴として送りたいデータ、あるいは欲しいデータが一度に全て送信/受信できるとは限らないためこのような構成になっています。
また、WAITが発生するような処理(送信/受信をセットにしている動作など)をUNITで分ける事で、キューの処理を待たせる事なく次のUNIT処理へと進む事ができるようになります。

例えばWebsocketのopeningハンドシェイクの場合はクライアントからヘッダ情報を受信した後は同じようにヘッダ情報をサーバーから送り返さないといけませんが、これをブロッキングモードで送信が完了するまで待っていると他の接続も巻き込んで処理が待たされる事になってしまいます。

この回避策として当ライブラリでは以下のUNIT処理のようにノンブロッキングモードで送信が行えるようにしています。
UNIT処理の一部を抜粋
// CREATEステータスのUNIT
{
    // 送信データの設定
    $p_param->protocol()->setSendingData(<送信データ>);

    return ProtocolStatusEnumForWebsocket::SEND->value;
}

// SENDステータスのUNIT
{
    // データ送信
    $w_ret = $p_param->protocol()->sending();

    // 送信中の場合は再実行
    if($w_ret === null)
    {
        $sta = $p_param->getStatusName();
        return $sta;
    }

    return null;
}
                    

実際にデータを送信するSENDステータスのひとつ前のUNIT処理(CREATEステータス)であらかじめ送信したいデータを設定しておき、送信を実行するUNIT処理(SENDステータス)でsendingメソッドの戻り値がnull以外になるまで繰り返しています。

sendingメソッドは設定された送信データを送信しきるまでnullを返す処理です。
getStatusNameメソッドは現在実行中のステータス名を取得するものです。

受信の場合も同じように受信サイズを設定するメソッドsetReceivingSizeと受信しきるまでnullを返すメソッドreceivingに分けています。

このようにUNITの処理の実装は極力処理の停滞(ブロッキング)がおきないように構成する事が求められます。

また、UNITステータスはリターン値によって遷移していきます。そしてキューの処理が全て完了したらnullを返すルールとなっています。
処理の流れはリターン値で制御できるので、同じような処理を繰り返し行ったり、分岐したいケースがある場合には処理を構造化しながらコントロールする事が可能になります。

このようなキューとステータスUNITをコントロールする役割をCycleDrivenManagerが担っています。

throwブレイク

このシステムでのthrowブレイクは、UNITのステータスを維持したまま処理を中断して待ち受け状態に戻る事を意味します。
ステータスUNIT内でUnitExceptionクラスを用いて例外(throw)を発行する事でthrowブレイクとなります。

UNIT定義クラス

UNITの処理はプロトコル/コマンド部を問わずIEntryUnitsインタフェースに従って実装する必要があります。
プロトコル部を作成する時はcraft:protocolコマンドを、コマンド部を作成する時はcraft:commandコマンドを実行してファイルを生成します。
生成されるファイルの種類は以下の通り。
  • UNIT定義クラス
  • キュー名定義のEnum
  • ステータス名定義のEnum
UNIT定義クラスの実装イメージは次の通りです。
プロトコル部のキュー名はProtocolQueueEnumで予約されているので、作成されたEnumファイルにはこの予約されたEnum値がエイリアス名として定義されます。

コマンド部のキュー名の定義は自由です。UNIT定義クラスの実装に合わせてキュー名を追加していく事になります。

■getQueueListメソッドの実装
プロトコル部、コマンド部それぞれで定義されたキュー名のEnum値をピックアップしてリストに追加します。
■getUnitListメソッドの実装
引数で渡されたキュー名と一致するリストを返します。プロトコル部、コマンド部それぞれで定義されたステータス名定義のEnum値をピックアップしてステータス名に対応するUNITメソッドとセットでリストを作成します。各キューの処理はSTARTステータスから始まるので必ず含める必要があります。

初期化クラス

初期化クラスはIInitSocketManagerインタフェースに従って実装する必要があります。
インタフェース実装に必要な内容は以下の通りです。

UNITパラメータクラス

SocketManagerParameterクラスやそれを継承しているクラスの事をUNITパラメータクラスと呼びます。
このインスタンスはUNIT処理の引数として渡されるもので、SocketManagerとの橋渡し役を担っています。

SocketManagerParameterクラスを継承する事でデータの送受信やディスクリプタ(クライアント接続子)の操作を行うための機能を提供すると同時に、アプリケーションで利用するグローバルエリアを管理する役割も担っています。
プロトコル部・コマンド部・コマンドディスパッチャー間で同じグローバルエリアの利用が可能です。
接続子単位でのグローバル管理が必要なデータに関しては、UNITパラメータクラス内のgetTempBuff/setTempBuffメソッドで取得・設定が可能です。
これを図にすると以下のようになります。
初期化クラス内のgetUnitParameterメソッドでインスタンスを返す必要がありますが、インスタンス化するタイミングは初期化クラスの内側/外側を問いません。

プロトコル部ではUNITパラメータクラス内でインプリメントされたIProtocolParameterインタフェースを返すprotocolメソッドを介して送受信用のメソッドを使う必要があります。
これに対してコマンド部ではUNITパラメータクラス内でインプリメントされたgetRecvDataメソッドを使って受信データを取得し、setSendStackメソッドを使って送信データを設定する流れが基本となります。

このような構成になっているのは以下の処理を前提としているからです。
・受信処理
受信データはプロトコル部で組み立てたものをコマンド部に渡します(コマンド部ではgetRecvDataメソッドを通して受信データを取得します)。
・送信処理
コマンド部で組み立てた送信データはプロトコル部で必要に応じて分解され、送信されます(コマンド部ではsetSendStackメソッドを通して送信データを引き渡します)。
この為コマンド部の処理では、ノンブロッキングの処理を意識する事なく通信データの送受信の実装が可能となります。

※protocolメソッドをコマンド部で呼び出すとUnitExceptionEnum::ECODE_METHOD_CALL_FAIL例外が発生してエラー終了しますのでご注意ください。
※具体的にどんなメソッドが存在するのかについてはSocketManagerParameterクラスの>> Referenceをご覧ください。

シリアライザー/アンシリアライザー

この処理はディスクリプタ内で管理された先入れ先出し方式である送受信スタックエリア(メモリ上のスタック領域とは無関係です)と深く関わっています。
送受信スタックエリア内ではシリアライズ化されたデータが保持されるため、データ取得時にはアンシリアライザーが、データ設定時にはシリアライザーが呼び出されます。
これを図にすると以下のようになります。

コマンドディスパッチャー

プロトコル部で受信したデータはUNITパラメータクラスのsetRecvStackメソッドを使って受信スタックへ格納する必要があります。
コマンドディスパッチャーはその受信データを解析してコマンド部に処理を振り分ける役割を担っています。

コマンド部から送信するデータはUNITパラメータクラスのsetSendStackメソッドを使って送信スタックへ格納する必要があります。
送信時はコマンドディスパッチャーの介入はなく、送信スタックに溜まったものをプロトコル部を経由して順次クライアントへ送信されます。

これを図にすると以下のようになります。

緊急停止時のコールバック

以下の場面で呼び出されます。
  • アライブチェック処理のタイムアウト
  • コマンドディスパッチャーで例外キャッチ時
  • 相手先による強制切断

ログライター

ライブラリ内を含めアプリケーションレイヤー内からも呼ばれるログ出力ハンドラーです。
アプリケーションレイヤーからはUNITパラメータクラスを通して呼ばれます。

メイン処理クラス

実装イメージは以下の通りです。
このうち初期設定ブロックはSocketManagerの準備処理として必要な部分です。
ポート設定ブロックではマルチサーバーの子サーバーとして起動する場合、親サーバーへの接続を行うためのconnectメソッドが使用される事があります。
ノンブロッキングループブロックは以下のイメージで動作します。
1つの接続に対してプロトコル部とコマンド部が連携し合って動作します。
このうち1回の周期(ループ)で実行されるのはプロトコル/コマンド部のそれぞれ1UNITずつとなります。
これが複数の接続になると以下のイメージになり、1つのサーバーで処理する単位になります。
そして複数のサーバーを起動している場合は以下のイメージになり、プロセスが順当に割り当てられると1つのサーバープロセスがCPUの各コア(このケースではコアが4つの場合)に割り当てられてデュアルで動作します。
※実際にサーバーをスケーリングする際には上記のイメージを基に計算/設計していく事になりますが、サーバー上には多数のプロセスが存在するため必ずしもSocketManagerのプロセスが4つとも常に割り当てられるとは限らないので、リソースや処理時間等の実測値を見ながら調整していく事になります。

おわりに

ここでご紹介した内容は特に新規プロジェクト開発をする時に必要になる情報ばかりですが、Websocket開発環境でもより高度な実装をするために必要な情報でもあります。
サーバー間通信を伴うマルチサーバーを構築する際の基礎的な内容でもありますので▶マルチサーバーの構成のページも合わせてご覧ください(マルチサーバーの構築を予定していない場合は読み飛ばしてもらっても構いません)。