【アーキテクチャ】

はじめに

このシステムではノンブロッキングモードによる実装を前提としており、TCP や UDP 通信を使った接続中クライアントの個別の挙動部分については、待ちを発生させないタイムシェアリングによる並行動作を可能にしているため、動的なプロセスのフォークやスレッドの生成を行わなくても実装できるようにしています。

こうする事でプロセスの絶対数を管理する事ができ、サーバー間通信を伴うマルチサーバーの構築も可能なので、サーバーリソースを極力抑えつつパフォーマンスを向上させ、スケーラビリティを考慮しながら高速に動作できるように設計しています。
また、プロセスの絶対数を管理する部分の考え方については、FastCGI を使ったAPサーバーのプロセス管理手法と一部似通っています。

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

レイヤー概念図

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

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

キューとUNITの関係

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

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

この回避策として当ライブラリでは以下の 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 定義クラスの実装イメージは次の通りです。
SOCKET-MANAGERフレームワークのイベント処理部分であるキューとUNITを定義するクラスの内部構造イメージ
プロトコル部のキュー名は ProtocolQueueEnum で予約されているので、作成された Enum ファイルにはこの予約された Enum 値がエイリアス名として定義されます。

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

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

初期化クラス

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

UNITパラメータクラス

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

SocketManagerParameter クラスを継承する事でデータの送受信やディスクリプタ(クライアント接続子)の操作を行うための機能を提供すると同時に、アプリケーションで利用するグローバルエリアを管理する役割も担っています。
プロトコル部・コマンド部・コマンドディスパッチャー間で同じグローバルエリアの利用が可能です。
接続子単位でのグローバル管理が必要なデータに関しては、UNIT パラメータクラス内の getTempBuff/setTempBuff メソッドで取得・設定が可能です。
これを図にすると以下のようになります。
SOCKET-MANAGERフレームワーク内でのグローバル管理領域のアクセス概念図
初期化クラス内の 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回の周期(ループ)で実行されるのはプロトコル/コマンド部のそれぞれ1 UNIT ずつとなります。
これが複数の接続になると以下のイメージになり、1つのサーバーで処理する単位になります。
複数のクライアント接続時のノンブロッキングループ動作イメージ
そして複数のサーバーを起動している場合は以下のイメージになり、プロセスが順当に割り当てられると1つのサーバープロセスが CPU の各コア(このケースではコアが4つの場合)に割り当てられてデュアルで動作します。
複数のサーバープロセス稼働時のノンブロッキングループ動作イメージ
※実際にサーバーをスケーリングする際には上記のイメージを基に計算/設計していく事になりますが、サーバー上の個々のプロセスを OS が必ずしも別々の CPU に割り当てるとは限らないため、リソースや処理時間等の実測値を見ながら調整していく事になります。

おわりに

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