SOCKET-MANAGERを使った自作サーバー開発の手引き＜プロトコルUNITクラスの実装＞

# はじめに

本フレームワークでは、通信プロトコル層を抽象化したインターフェースを採用しており、TCPやUDPなどのトランスポートプロトコルを柔軟に切り替えられる構成になっています。
これにより、開発時や運用時に用途に応じたプロトコル選択が可能です。

さらに、プロトコル処理のスキャフォールディング（コード自動生成）機能も備えており、テンプレートベースでの開発を通じて、高い拡張性と保守性を確保しながら、実装作業の効率化を実現します。

プロトコルUNITクラスを生成するコマンドは以下の通り。

```terminal:コマンド実行（ProtocolForTestというクラス名を指定する場合）
> php worker craft:protocol ProtocolForTest

[success] プロトコルUNITクラスの生成に成功しました (ProtocolForTest)
[success] プロトコルUNITのキュー名Enumの生成に成功しました (ProtocolForTestQueueEnum)
[success] プロトコルUNITのステータス名Enumの生成に成功しました (ProtocolForTestStatusEnum)
```

コマンドを実行する事で`app/ProtocolUnits`の場所に以下３つのファイルが生成されます。

<dl>
    <dt>・ProtocolForTest.php</dt>
    <dd>
        <code>IEntryUnits</code>インターフェイスがimplementsされたプロトコルUNITクラスです。<br />
        ここにプロトコル処理のイベントハンドラを実装します。<br />
    </dd>
    <dt>・ProtocolForTestQueueEnum.php</dt>
    <dd>
        ここにプロトコル処理のキュー（イベント）名を定義します。<br />
    </dd>
    <dt>・ProtocolForTestStatusEnum.php</dt>
    <dd>
        ここにプロトコル処理のステータス（UNIT）名を定義します。<br />
    </dd>
</dl>

このフレームワーク環境では、キュー名とステータス名をEnumで定義する事を推奨しているので、それぞれのEnumファイルに分けて出力されます。

以降では生成されたファイルの内容を見ていきます。


# キュー名の定義

`ProtocolForTestQueueEnum.php`のファイルは、イベントに対応するキュー名を定義するEnumファイルです。
プロトコルUNITのキュー名はあらかじめ`ProtocolQueueEnum`で予約されていますので、生成されたEnumファイルにはその内容が代入されています。
定義済みのキュー名は以下の通り。

<dl>
    <dt>ProtocolForTestQueueEnum::ACCEPT</dt>
    <dd>クライアントからの接続要求に対するアクセプト時に呼ばれる</dd>
    <dt>ProtocolForTestQueueEnum::CONNECT</dt>
    <dd>当該サーバーから他のエンドポイントへ接続する時に呼ばれる</dd>
    <dt>ProtocolForTestQueueEnum::RECV</dt>
    <dd>通信データを受信する時に呼ばれる</dd>
    <dt>ProtocolForTestQueueEnum::SEND</dt>
    <dd>通信データを送信する時に呼ばれる</dd>
    <dt>ProtocolForTestQueueEnum::CLOSE</dt>
    <dd>切断シーケンスを走らせる時に呼ばれる</dd>
    <dt>ProtocolForTestQueueEnum::ALIVE</dt>
    <dd>アライブチェックを走らせる時に呼ばれる</dd>
</dl>

これらのキュー名はプロトコルUNITクラスで定義されるイベントごとのUNITの集合を特定するためのものです。
今回のケースでは`ProtocolForTest`クラス内でキューとUNITの紐づけを行います。
紐づけの方法は以下の<a href="#プロトコルunitクラスの実装">>> プロトコルUNITクラスの実装</a>の項で説明しています。

:::note info
必ずしも全てのキューを使う必要はありませんが、オリジナルプロトコルを開発する場合は少なくともSENDキューとRECVキューが必要になります。
:::


# ステータス名の定義

`ProtocolForTestStatusEnum.php`のファイルは、プロトコルUNITのステータス名を定義するEnumファイルです。
プロトコルUNITのステータス名であらかじめ予約されているものは`StatusEnum::START`だけで、生成されたEnumファイルにはその内容が代入されています。
それ以外のステータス名は自由に定義する事ができます。
定義済みのステータス名は以下の通り。

<dl>
    <dt>ProtocolForTestStatusEnum::START</dt>
    <dd>
        同じキューに登録されているUNITの集合のうち一番最初に実行されるステータスです。<br />
        UNITは必ずSTARTステータスから始まるルールになっています。<br />
    </dd>
</dl>

これらのステータス名はプロトコルUNITクラスで定義したUNITに紐づくものなので、今回のケースでは先ほど生成した`ProtocolForTest`クラス内でUNITとステータス名の紐づけを行います。
紐づけの方法は以下の<a href="#プロトコルunitクラスの実装">>> プロトコルUNITクラスの実装</a>の項で説明しています。

:::note info
ここで定義したステータス名は異なるキューで再利用が可能です。
:::


# プロトコルUNITクラスの実装

`ProtocolForTest.php`のファイルにはUNIT定義を含めた各種メソッドが実装されています。
各メソッドの仕様と実装例は以下の通り。

## ➤キューリストの取得

フレームワークは以下のメソッドをコールして必要なキューのリストを登録します。

```php:シグネチャ
【メソッド】getQueueList(): array

【パラメータ】なし

【戻り値】array - キュー名のリスト
```

例えば予約されている全てのキューを利用する場合、以下のように実装します。

```php:メソッドの実装例
protected const QUEUE_LIST = [
    ProtocolForTestQueueEnum::ACCEPT->value,   // アクセプトを処理するキュー
    ProtocolForTestQueueEnum::RECV->value,     // 受信処理のキュー
    ProtocolForTestQueueEnum::SEND->value,     // 送信処理のキュー
    ProtocolForTestQueueEnum::CLOSE->value,    // 切断処理のキュー
    ProtocolForTestQueueEnum::ALIVE->value     // アライブチェック処理のキュー
];
public function getQueueList(): array
{
    return (array)static::QUEUE_LIST;
}
```

## ➤ステータスUNITリストの取得

以下のメソッドがフレームワーク内部でコールされる事によって、キューとUNITの紐づけが登録されます。

```php:シグネチャ
【メソッド】getUnitList(string $p_que): array

【パラメータ】
    $p_que - string - 必須 - キュー名

【戻り値】array - キューごとのUNIT集合のリスト（連想配列）
```

フレームワーク内部では`getQueueList`メソッドで取得したキュー名を元に、`getUnitList`メソッドがコールされるため、引数にはキュー名が渡されます。
例えば`SEND`キューと`RECV`キューにそれぞれ２つずつUNITの集合を登録する場合、以下のように引数で与えられたキュー名に対応するリストを返す必要があります。

```php:メソッドの実装例
public function getUnitList(string $p_que): array
{
    $ret = [];

    // SENDキューのUNIT集合を登録
    if($p_que === ProtocolForTestQueueEnum::SEND->value)
    {
        // STARTステータスとUNIT（getSendStart）の紐づけ
        $ret[] = [
            'status' => ProtocolForTestStatusEnum::START->value,
            'unit' => $this->getSendStart()
        ];
        // SEND_COMPLETEステータスとUNIT（getSendComplete）の紐づけ
        $ret[] = [
            'status' => ProtocolForTestStatusEnum::SEND_COMPLETE->value,
            'unit' => $this->getSendComplete()
        ];
    }

    // RECVキューのUNIT集合を登録
    if($p_que === ProtocolForTestQueueEnum::RECV->value)
    {
        // STARTステータスとUNIT（getRecvStart）の紐づけ
        $ret[] = [
            'status' => ProtocolForTestStatusEnum::START->value,
            'unit' => $this->getRecvStart()
        ];
        // RECV_COMPLETEステータスとUNIT（getRecvComplete）の紐づけ
        $ret[] = [
            'status' => ProtocolForTestStatusEnum::RECV_COMPLETE->value,
            'unit' => $this->getRecvComplete()
        ];
    }
    .
    .
    .
    return $ret;
}
```

## ➤ステータスUNITの実装

ここでは`getUnitList`メソッド内で紐づけたUNITを定義します。

```php:シグネチャ
【メソッド】<任意のメソッド名>(): Closure|string|null

【パラメータ】なし

【戻り値】
    Closure|string
    - ステータスUNITの定義: Closure
        パラメータ:
            $p_param - SocketManagerParameter - 必須 - UNITパラメータクラスのインスタンス
                各イベントハンドラで共通の引数として使用されるインスタンス。
                SocketManagerParameterクラスを継承した拡張クラスを指定する事も可能。
        戻り値: string|null
            - 遷移先がある場合: string
                遷移先のキュー名
            - 遷移先がない（終了する）場合: null
    - ステータスUNITの定義: string
        ヘルパー関数などの関数名
```

以下では`SEND`キューのUNIT集合と`RECV`キューのUNIT集合をポーリングUNITを使って実装した例をご紹介します。
ポーリングUNITの詳細については以下ページの説明をご覧ください。

https://qiita.com/glasses_seven/items/2f16c3ab748ccabe764d

<dl>
    <dt>【SENDキューのUNIT集合】</dt>
    <dd>
        ここでは、スタックエリアの送信データをポーリングUNIT（getSendComplete）を使って送信する例をご紹介しています。<br />

```php:SENDキューのUNIT集合の実装例
protected function getSendStart()
{
    return function(ParameterForTest $p_param): ?string
    {
        // スタックエリアから送信データを取得する
        $send_data = $p_param->protocol()->getSendData();

        // 送信データの設定
        $p_param->protocol()->setSendingData($send_data);

        // SEND_COMPLETEステータス（getSendComplete）のUNITへ遷移
        return ProtocolForTestStatusEnum::SEND_COMPLETE->value;
    };
}
protected function getSendComplete()
{
    return function(ParameterForTest $p_param): ?string
    {
        // データ送信
        $w_ret = $p_param->protocol()->sending();
    
        // nullの場合は送信中のためポーリングを続ける
        if($w_ret === null)
        {
            // 自身のステータス名を返してポーリングする
            $sta = $p_param->getStatusName();
            return $sta;
        }

        // nullを返して終了する
        return null;
    };
}
```

</dd>
    <dt>【RECVキューのUNIT集合】</dt>
    <dd>
ここでは、仮に10バイトの固定データをポーリングUNIT（getRecvComplete）を使って受信する例をご紹介しています。<br />
（実際には、実装する各プロトコルフォーマットに合わせてデータ長を取得する必要があります）

```php:RECVキューのUNIT集合の実装例
protected function getRecvStart()
{
    return function(ParameterForTest $p_param): ?string
    {
        // 受信データサイズの設定
        $p_param->protocol()->setReceivingSize(10);

        // RECV_COMPLETEステータス（getRecvComplete）のUNITへ遷移
        return ProtocolForTestStatusEnum::RECV_COMPLETE->value;
    };
}
protected function getRecvComplete()
{
    return function(ParameterForTest $p_param): ?string
    {
        // データ受信
        $w_ret = $p_param->protocol()->receiving();
    
        // nullの場合は受信中のためポーリングを続ける
        if($w_ret === null)
        {
            // 自身のステータス名を返してポーリングする
            $sta = $p_param->getStatusName();
            return $sta;
        }

        // 受信したデータを受信スタックへ設定
        $p_param->setRecvStack($w_ret);

        // nullを返して終了する
        return null;
    };
}
```

<code>getRecvComplete</code>メソッド（UNIT）内で<code>$p_param->setRecvStack()</code>メソッドを使って受信データを設定する事で、コマンドディスパッチャーやコマンドUNITに受信データを引き渡す事ができます。
    </dd>
</dl>

:::note info
送受信メソッドには以下のページでご紹介したprotocol()のメソッドチェーンを使用しています。
UNITパラメータクラスには以下のページで使用したParameterForTestを指定しています。
:::

https://qiita.com/glasses_seven/items/42f8175aec51fe632d52


# おわりに

プロトコルUNITクラスはプロトコルの実装を担う部分なので、通信データとして扱うデータは基本的にバイナリデータである事に注意してください。
そのため、メソッドチェーンの仲介役である`protocol()`を介して送受信を行う必要があります。

生成されたクラスのインスタンスは、メイン処理クラス内で`$manager->setProtocolUnits()`メソッドに引き渡す事で適用されます。
複数のプロトコルUNITクラスやメイン処理クラスを用意している場合は、サーバーの実装内容によって最適なインスタンスを動的、あるいは静的に適用する事で柔軟なサーバー構築が可能になります。