【アーキテクチャ】
はじめに
SOCKET-MANAGER Framework は、CUEIアーキテクチャ(キューイ、通称キュー) をビルトイン採用した、軽量で拡張性のある通信フレームワークです。
CUEIアーキテクチャは、以下の4つの要素を統合した設計思想を指します。
CUEIは以下の4つの柱で構成されます。
CUEIアーキテクチャを構成する4大要素
それぞれが独立しつつも相互に連携し、フレームワーク全体を支える基盤を形成しています。
これを組み合わせることで、CUEI/O(キューアイオー) として「開発から運用まで」を一貫してカバーできます。
SOCKET-MANAGER Launcherを組み合わせることでCUEIはCUEI/Oへ拡張されます。
CUEI/O──開発と運用を統合する拡張モデル
CUEIアーキテクチャは、以下の4つの要素を統合した設計思想を指します。
- Communication(通信抽象化):多様なプロトコルを統一的に扱う
- Union(共有基盤):処理全体を束ねる共有基盤
- Event(非同期処理):イベント駆動型の非同期処理
- IPC(サーバー間通信):マルチサーバー構成・分散処理
CUEIは以下の4つの柱で構成されます。
四象限モデル(CUEIの4要素)
CUEIアーキテクチャは、Communication / Union / Event / IPC の4要素を四象限モデルとして整理できます。それぞれが独立しつつも相互に連携し、フレームワーク全体を支える基盤を形成しています。
当フレームワークで置き換えると…
- ・Communication(通信抽象化)
-
TCP・UDP・WebSocket・独自プロトコルなど多様な通信方式を抽象化する事でビジネスロジックとのシームレスな分離が可能。
- ・Union(共有基盤)
-
UNITパラメータによる統合コンテキスト。イベント処理全体のハブとして機能し、接続情報や送受信データを一元管理。
送受信データに関してはFIFO方式バッファを採用する事でイベント多重起動を防止。
- ・Event(非同期処理)
-
独自のイベントループとコルーチンによる軽量なイベント駆動処理を実装する事で、動的なプロセスやスレッドに頼らない設計が可能。
FIFOバッファの送受信データを順次取り出し、ビルトインの状態遷移制御により処理順序を保障。
- ・IPC(サーバー間通信)
-
サーバー間通信を前提としたマルチサーバー構成とスケーラビリティの確保。
INETソケットを使う事でスケールアップ/スケールアウト時も統一的なインターフェースを提供する事が可能。
CUEI/O(開発から運用まで)
さらに、SOCKET-MANAGER には SOCKET-MANAGER Launcher というサーバーリソース監視付きのサービス管理ランチャーを用意しています。これを組み合わせることで、CUEI/O(キューアイオー) として「開発から運用まで」を一貫してカバーできます。
- CUEI = 開発思想(通信・共有基盤・非同期処理・サーバー間通信)
- CUEI/O = 開発+運用(Operation)を含む完全版
SOCKET-MANAGER Launcherを組み合わせることでCUEIはCUEI/Oへ拡張されます。
レイヤー概念図
CUEIアーキテクチャの思想を具体的に実装した構造が、以下の「レイヤー概念図」です。
ここではプロトコル部とコマンド部の分離、UNITパラメータの役割、CycleDrivenManagerによる制御など、ノンブロッキングモードによる実装を前提とした仕組みを詳しく解説していきます。
サーバーのアプリケーションの部分はプロトコル部とコマンド部に大きく分かれています。
最初にそれらをライブラリ内の
プロトコル部とコマンド部に分けているのは、それぞれを自由に入れ替える事ができるようにするためです。
作成したクライアントのプロトコルに合わせてプロトコル部を入れ替えたり、サーバーサイドのコンテンツを切り替えるためにコマンド部を入れ替えたりする事で様々な組み合わせのサーバーを構築する事が可能です。
ここではプロトコル部とコマンド部の分離、UNITパラメータの役割、CycleDrivenManagerによる制御など、ノンブロッキングモードによる実装を前提とした仕組みを詳しく解説していきます。
サーバーのアプリケーションの部分はプロトコル部とコマンド部に大きく分かれています。
最初にそれらをライブラリ内の
CycleDrivenManager のプロトコル部とコマンド部へ取り込ませてから使う事になります。プロトコル部とコマンド部に分けているのは、それぞれを自由に入れ替える事ができるようにするためです。
作成したクライアントのプロトコルに合わせてプロトコル部を入れ替えたり、サーバーサイドのコンテンツを切り替えるためにコマンド部を入れ替えたりする事で様々な組み合わせのサーバーを構築する事が可能です。
キューとUNITの関係
まずは
図にすると以下のようなイメージになります。 通信データの送受信の特徴として送りたいデータ、あるいは欲しいデータが一度に全て送信/受信できるとは限らないためこのような構成になっています。
また、WAIT が発生するような処理(送信/受信をセットにしている動作など)を UNIT で分ける事で、キューの処理を待たせる事なく次の UNIT 処理へと進む事ができるようになります。
例えば Websocket の opening ハンドシェイクの場合はクライアントからヘッダ情報を受信した後は同じようにヘッダ情報をサーバーから送り返さないといけませんが、これをブロッキングモードで送信が完了するまで待っていると他の接続も巻き込んで処理が待たされる事になってしまいます。
この回避策として当ライブラリでは以下の UNIT 処理のようにノンブロッキングモードで送信が行えるようにしています。
実際にデータを送信する
※
※
受信の場合も同じように受信サイズを設定するメソッド
このように UNIT の処理の実装は極力処理の停滞(ブロッキング)がおきないように構成する事が求められます。
また、UNIT ステータスはリターン値によって遷移していきます。そしてキューの処理が全て完了したら null を返すルールとなっています。
処理の流れはリターン値で制御できるので、同じような処理を繰り返し行ったり、分岐したいケースがある場合には処理を構造化しながらコントロールする事が可能になります。
このようなキューとステータスUNITをコントロールする役割を
ステータスUNIT内で
CycleDrivenManager の動きを理解するためにキューとステータス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 の処理はプロトコル/コマンド部を問わず
プロトコル部を作成する時は
生成されるファイルの種類は以下の通り。
プロトコル部のキュー名は
コマンド部のキュー名の定義は自由です。UNIT 定義クラスの実装に合わせてキュー名を追加していく事になります。
IEntryUnits インターフェースに従って実装する必要があります。プロトコル部を作成する時は
craft:protocol コマンドを、コマンド部を作成する時は craft:command コマンドを実行してファイルを生成します。生成されるファイルの種類は以下の通り。
- UNIT 定義クラス
- キュー名定義の Enum
- ステータス名定義の Enum
プロトコル部のキュー名は
ProtocolQueueEnum で予約されているので、作成された Enum ファイルにはこの予約された Enum 値がエイリアス名として定義されます。コマンド部のキュー名の定義は自由です。UNIT 定義クラスの実装に合わせてキュー名を追加していく事になります。
- ■ getQueueList メソッドの実装
- プロトコル部、コマンド部それぞれで定義されたキュー名の Enum 値をピックアップしてリストに追加します。
- ■ getUnitList メソッドの実装
- 引数で渡されたキュー名と一致するリストを返します。プロトコル部、コマンド部それぞれで定義されたステータス名定義の Enum 値をピックアップしてステータス名に対応する UNIT メソッドとセットでリストを作成します。各キューの処理は
STARTステータスから始まるので必ず含める必要があります。
初期化クラス
初期化クラスは
インターフェース実装に必要な内容は以下の通りです。
このインスタンスは UNIT 処理の引数として渡されるもので、
プロトコル部・コマンド部・コマンドディスパッチャー間で同じグローバルエリアの利用が可能です。
接続子単位でのグローバル管理が必要なデータに関しては、UNIT パラメータクラス内の
これを図にすると以下のようになります。
初期化クラス内の
プロトコル部では UNIT パラメータクラス内でインプリメントされた
これに対してコマンド部では UNIT パラメータクラス内でインプリメントされた
このような構成になっているのは以下の処理を前提としているからです。
※ protocol メソッドをコマンド部で呼び出すと
※ 具体的にどんなメソッドが存在するのかについては
送受信スタックエリア内ではシリアライズ化されたデータが保持されるため、データ取得時にはアンシリアライザーが、データ設定時にはシリアライザーが呼び出されます。
これを図にすると以下のようになります。
コマンドディスパッチャーはその受信データを解析してコマンド部に処理を振り分ける役割を担っています。
コマンド部から送信するデータは UNIT パラメータクラスの
送信時はコマンドディスパッチャーの介入はなく、送信スタックに溜まったものをプロトコル部を経由して順次クライアントへ送信されます。
これを図にすると以下のようになります。
アプリケーションレイヤーからは UNIT パラメータクラスを通して呼ばれます。
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 パラメータクラスを通して呼ばれます。
メイン処理クラス
実装イメージは以下の通りです。
このうち初期設定ブロックは
ポート設定ブロックではマルチサーバーの子サーバーとして起動する場合、親サーバーへの接続を行うための
ノンブロッキングループブロックは以下のイメージで動作します。
1つの接続に対してプロトコル部とコマンド部が連携し合って動作します。
このうち1回の周期(ループ)で実行されるのはプロトコル/コマンド部のそれぞれ1 UNIT ずつとなります。
これが複数の接続になると以下のイメージになり、1つのサーバーで処理する単位になります。
そして複数のサーバーを起動している場合は以下のイメージになり、プロセスが順当に割り当てられると1つのサーバープロセスが CPU の各コア(このケースではコアが4つの場合)に割り当てられてデュアルで動作します。
※ 実際にサーバーをスケーリングする際には上記のイメージを基に計算/設計していく事になりますが、サーバー上の個々のプロセスを OS が必ずしも別々の CPU に割り当てるとは限らないため、リソースや処理時間等の実測値を見ながら調整していく事になります。通常は OS が自動的に CPU 割り当てを行いますが、SOCKET-MANAGER Launcher を利用することで任意の CPU にプロセスを割り当てることが可能となり、プロセス間の競合を抑制しながらより効率的なリソース活用が行えます。
このうち初期設定ブロックは
SocketManager の準備処理として必要な部分です。ポート設定ブロックではマルチサーバーの子サーバーとして起動する場合、親サーバーへの接続を行うための
connect メソッドが使用される事があります。ノンブロッキングループブロックは以下のイメージで動作します。
1つの接続に対してプロトコル部とコマンド部が連携し合って動作します。
このうち1回の周期(ループ)で実行されるのはプロトコル/コマンド部のそれぞれ1 UNIT ずつとなります。
これが複数の接続になると以下のイメージになり、1つのサーバーで処理する単位になります。
そして複数のサーバーを起動している場合は以下のイメージになり、プロセスが順当に割り当てられると1つのサーバープロセスが CPU の各コア(このケースではコアが4つの場合)に割り当てられてデュアルで動作します。
※ 実際にサーバーをスケーリングする際には上記のイメージを基に計算/設計していく事になりますが、サーバー上の個々のプロセスを OS が必ずしも別々の CPU に割り当てるとは限らないため、リソースや処理時間等の実測値を見ながら調整していく事になります。通常は OS が自動的に CPU 割り当てを行いますが、SOCKET-MANAGER Launcher を利用することで任意の CPU にプロセスを割り当てることが可能となり、プロセス間の競合を抑制しながらより効率的なリソース活用が行えます。
おわりに
ここでご紹介した内容は特に新規プロジェクト開発をする時に必要になる情報ばかりですが、Websocket 開発環境でもより高度な実装をするために必要な情報でもあります。
サーバー間通信を伴うマルチサーバーを構築する際の基礎的な内容でもありますので▶マルチサーバーの構成のページも合わせてご覧ください(マルチサーバーの構築を予定していない場合は読み飛ばしてもらっても構いません)。
サーバー間通信を伴うマルチサーバーを構築する際の基礎的な内容でもありますので▶マルチサーバーの構成のページも合わせてご覧ください(マルチサーバーの構築を予定していない場合は読み飛ばしてもらっても構いません)。










