Websocketサーバーの開発環境を作ってみました

# はじめに

この記事でご紹介している環境はデモ版としてご紹介させて頂いたWebsocketサーバーからコマンド部を含めWebsocketに不要な部分を取り除いて再構築したものです。

プロトコルの主要な部分は実装済みなので、コマンド部を自分で作成しながらブラウザを使って動作確認できますので、新規でプロジェクトを起こす場合に比べてソケット通信初心者向けの環境と言えます。

デモ版の環境については以下のページでご紹介しています。

https://qiita.com/glasses_seven/items/7a490dc8694348505a53/

以降ではインストール方法と実装例を見ながら進めていきます。

# 環境

<dl>
    <dt>プラットフォーム</dt>
    <dd>Windows10</dd>
    <dt>統合環境</dt>
    <dd>Xampp v3.3.0</dd>
    <dt>ブラウザ</dt>
    <dd>Chrome v123.0.6312.123</dd>
    <dt>言語</dt>
    <dd>PHP v8.2.4（v8.1.0以降）</dd>
</dl>

:::note info
ここではXAMPP環境を使っている前提で説明していますが、デフォルトとは別の場所に格納している場合やLinux等の別の環境で利用されている場合は適宜読み替えてください。
:::

# インストール

以下のコマンドでインストールできます。

```terminal:Websocketサーバー開発環境のインストール
> composer create-project socket-manager/websocket-project <インストール先のディレクトリ名>
```
:::note info
GitHubから直接ダウンロードされる場合は以下のページからどうぞ。
:::

https://github.com/socket-manager/websocket-project/

<br />
それでは上記でインストールしたプロジェクトルートに移動して`php worker`コマンドを実行してみます。

```terminal:Usage表示
> php worker

SOCKET-MANAGER Framework 1.0.0
Usage:
  command [arguments]

 main
  app:websocket-server                           Websocketサーバー
 craft
  craft:init <初期化クラス名>                     初期化クラスの生成
  craft:parameter <UNITパラメータクラス名>        UNITパラメータクラスの生成
  craft:protocol <プロトコルUNIT定義のクラス名>   プロトコルUNIT定義のクラスとステータス名Enumの生成
  craft:command <コマンドUNIT定義のクラス名>      コマンドUNIT定義のクラスとキュー／ステータス名Enumの生成
  craft:main <メイン処理のクラス名>               メイン処理クラスの生成
  craft:setting <設定ファイル名>                  設定ファイルの生成
  craft:locale <メッセージファイル名>             メッセージファイルの生成
```

ご覧のように`app:websocket-server`が含まれている事が確認できます。
これを以下のように実行する事でサーバーを起動できます。

```terminal:Websocketサーバーの起動
> php worker app:websocket-server [<ポート番号>]
```

コマンドの詳しい使い方は以下のページでご紹介しています。

https://qiita.com/glasses_seven/items/d712808b985aa5253d05/


# 実装例

ここではコマンド処理部分のチャットメッセージを実装してみます。


### クライアントの実装

このプロジェクトでは`client`ディレクトリに検証用として最低限の実装でファイルを同梱しています。
それでは`client`ディレクトリ内の`test.js`を編集します。
`test.js`内には以下のように`onopen`メソッドがあるのでここに処理を入れます。

```javascript:onopenメソッドの実装:app/client/test.js
// Websocket接続
websocket = new WebSocket(uri);

/**
 * 接続完了イベント
 * 
 * @param {*} event イベントインスタンス
 * @returns 
 */
websocket.onopen = function(event)
{
    let data =
    {
          'cmd': 'message'
        , 'comment': 'テスト'
    };
    websocket.send(JSON.stringify(data));
};
```

`onopen`メソッドはWebsocketの接続が確立した時に呼び出されるイベントです。
上記のようにコマンド名は`message`、送信コメントは`テスト`にしてJSONデータを送信する処理を入れています。

そして以下のように`onmessage`メソッドで受信したデータを受け取れますので受信したデータはコンソールに表示されるようになっています。

```javascript:onmessageメソッド:app/client/test.js
/**
 * データ受信イベント
 * 
 * @param {*} event イベントインスタンス
 * @returns 
 */
websocket.onmessage = function(event)
{
    let data = JSON.parse(event.data);

    console.dir(data);
};
```


### サーバーの実装

まずは`CommandForWebsocketQueueEnum.php`のファイルに今回追加するコマンド名を定義します。

```php:キュー名の追加：app/CommandUnits/CommandForWebsocketQueueEnum.php
<?php
/**
 * コマンド部のキュー名のENUMファイル
 * 
 * Websocket用
 */

namespace App\CommandUnits;


/**
 * コマンド部のキュー名定義
 * 
 * Websocket用
 */
enum CommandForWebsocketQueueEnum: string
{
    case MESSAGE = 'message';
}
```

次に`CommandForWebsocket.php`を編集します。

```php:ステータスUNITの実装:app/CommandUnits/CommandForWebsocket.php
class CommandForWebsocket implements IEntryUnits
{
    /**
     * @var const QUEUE_LIST キュー名のリスト
     */
    protected const QUEUE_LIST = [
        CommandForWebsocketQueueEnum::MESSAGE->value,
    ];

    /**
     * キューリストの取得
     * 
     * @return array キュー名のリスト
     */
    public function getQueueList(): array
    {
        return (array)static::QUEUE_LIST;
    }

    /**
     * ステータスUNITリストの取得
     * 
     * @param string $p_que キュー名
     * @return array キュー名に対応するUNITリスト
     */
    public function getUnitList(string $p_que): array
    {
        $ret = [];

        if($p_que === CommandForWebsocketQueueEnum::MESSAGE->value)
        {
            $ret[] = [
                'status' => CommandForWebsocketStatusEnum::START->value,
                'unit' => $this->getMessageStart()
            ];
        }

        return $ret;
    }

    protected function getMessageStart()
    {
        return function(ParameterForWebsocket $p_param): ?string
        {
            $p_param->logWriter('debug', ['COMMAND:MESSAGE' => 'START']);

            $w_ret = $p_param->getRecvData();
            $msg = $w_ret['data'];

            $data =
            [
                'data' => $msg
            ];
            $p_param->setSendStackAll($data);

            return null;
        };
    }
}
```

追加したポイントは以下の通り。
<dl>
    <dt>■QUEUE_LISTへEnum値を追加</dt>
    <dd>MESSAGEというキュー名を追加</dd>
    <dt>■getUnitListメソッドの実装</dt>
    <dd>MESSAGEコマンドに対応するUNIT（メソッド）処理を登録</dd>
    <dt>■getMessageStartメソッドの実装（※）</dt>
    <dd>受信したデータを配信する処理を実装（上記ソース内のlogWriterメソッドでログ出力できます）</dd>
</dl>

:::note info
サーバーから`setSendStackAll`メソッドを使ってデータを送信する時、データ構造に`data`というキーを噛ませていますが、これはWebsocketの切断フレームの仕様が特殊なため敢えてこのような構造にしています。コマンド部でデータを送受信する時は`data`キーを噛ませる事で切断フレームを含めたデータ構造を一律に保つようにしています。
:::

:::note info
引数の`ParameterForWebsocket`クラスは`SocketManagerParameter`クラスを継承しています。詳しくは`SocketManagerParameter`クラスの[>>`Reference`](https://socket-manager.github.io/document/reference/classes/SocketManager-Library-SocketManagerParameter.html "SocketManagerParameter Reference")をご覧ください。
:::


# 動作確認

以下のコマンドを実行してサーバーを10000ポートで起動します。

```terminal:Websocketサーバーの起動
> php worker app:websocket-server 10000
```

次にクライアントをブラウザで開きます。
`client/test.html`をブラウザへドラッグ＆ドロップした上でF12を押して以下の画面を開いてください。

![browser.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619905/74edff98-3517-92aa-ee7c-4550054059de.png)

この状態で接続ボタンをクリックすると以下のようにコンソール画面でメッセージコマンドが返ってきている事が確認できます。

![browser_exec.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3619905/277d288b-d448-006b-b65a-d8a0910913ff.png)

そしてさらにブラウザをもう一つ開いて同じ操作をすると二つのブラウザウインドウに同じメッセージが配信されている事が確認できますので色々と試してみてください。

# ログ出力について

以下のディレクトリ構成でログファイルが出力されます。

```text:ログ出力先のディレクトリ
/logs
    /socket-manager-log     Websocketサーバーのログ
```

ファイルの命名規則は次の通り。
<dl>
    <dt>■Websocketサーバーのログ</dt>
    <dd><日付文字列（"Ymd"形式）>_W<ポート番号>.log</dd>
</dl>


# おわりに

コマンド処理は上記で実装したように以下の流れが基本となります。

<dl>
    <dt>１．受信データの取得</dt>
    <dd>getRecvDataメソッドで取得</dd>
    <dt>２．送信データの作成</dt>
    <dd>シリアライズ前のデータを作成する</dd>
    <dt>３．送信データの設定</dt>
    <dd>setSendStack、setSendStackAllメソッドで設定</dd>
</dl>

Websocketサーバー開発環境では上記のようにコマンド部の処理を追加していく事でサーバーコンテンツを作成していく事になります。

以下のページであらかじめアーキテクチャの内容を理解して頂いた方がより高度な実装が可能になりますので興味のある方はご覧ください。

https://socket-manager.github.io/document/architecture.html

ホスト名やポート番号等の設定ファイルの詳細は以下のページでまとめています。

https://socket-manager.github.io/document/extra-demo-setting.html

自作のプロトコルを実装する必要がある場合は以下の記事をご覧ください。

https://qiita.com/glasses_seven/items/d712808b985aa5253d05/

ここでご紹介したプロジェクトはLaravelと連携できます。
ご興味のある方は以下の記事をご覧ください。

https://qiita.com/glasses_seven/items/39ee921d754ed83585b3/