プロトコルのカスタマイズ方法

実際には、自分のプロトコルを定義することは比較的簡単です。簡単なプロトコルは一般的に以下の二つの部分で構成されます:

  • データ境界を区別する識別子
  • データ形式の定義

一例

プロトコル定義

ここではデータ境界を区別する識別子を改行文字"\n"と仮定します(リクエストデータ自体に改行文字を含めないことに注意してください)。データ形式はJSONで、以下はこのルールに準拠したリクエストパケットの例です。

{"type":"message","content":"hello"}

注意:上記のリクエストデータの末尾には改行キャラクターが含まれています(PHPではダブルクォーテーションストリングの"\n"で表されます)。これはリクエストの終了を示しています。

実装手順

Workermanで上記のプロトコルを実装する場合、プロトコルの名前をJsonNLとし、プロジェクト名をMyAppとした場合、以下の手順が必要です。

  1. プロトコルファイルをプロジェクトのProtocolsフォルダに配置します。例えば、ファイルはMyApp/Protocols/JsonNL.phpとします。

  2. JsonNLクラスを実装します。これにはnamespace Protocols;を名前空間として使用し、input、encode、decodeの三つの静的メソッドを必ず実装します。

注意:workermanは自動的にこれらの三つの静的メソッドを呼び出し、パケットの分割、解凍、パッケージ化を実現します。具体的なフローについては、以下の実行フローの説明を参考にしてください。

workermanとプロトコルクラスのインタラクションフロー

  1. クライアントがサーバーにデータパケットを送信したと仮定します。サーバーはデータ(部分的なデータの可能性あり)を受信するとすぐにプロトコルのinputメソッドを呼び出し、このパケットの長さを検出します。inputメソッドは工作機械フレームワークに$lengthという長さの値を返します。
  2. workermanフレームワークはこの$lengthの値を受け取り、現在のデータバッファ内で$lengthの長さのデータが既に受信されているかを判断します。受信していない場合は、$lengthの長さに満たないデータがバッファ内に存在する限り、さらにデータを待ち続けます。
  3. バッファ内のデータの長さが十分である場合、workermanはバッファから$lengthの長さのデータを切り出し(すなわちパケットの分割)、プロトコルのdecodeメソッドを呼び出して解凍します。解凍後のデータは$dataとなります。
  4. 解凍後、workermanはデータ$dataをコールバックonMessage($connection, $data)の形でビジネスロジックに渡します。ビジネスロジックではonMessage内で$data変数を使用してクライアントから送信された完全で解凍されたデータを取得できます。
  5. onMessage内でビジネスロジックが$connection->send($buffer)メソッドを呼び出してクライアントにデータを送信する必要がある場合、workermanは自動的にプロトコルのencodeメソッドを利用して$bufferパッケージ化し、再びクライアントに送信します。

具体的な実装

MyApp/Protocols/JsonNL.phpの実装

namespace Protocols;
class JsonNL
{
    /**
     * パケットの完全性をチェックする
     * パケットの長さが得られれば、バッファ内のパケットの長さを返し、そうでなければ0を返してデータを待ち続ける
     * プロトコルに問題がある場合は-1を返すことができ、その場合は現在のクライアント接続が切断される
     * @param string $buffer
     * @return int
     */
    public static function input($buffer)
    {
        // 改行文字"\n"の位置を取得
        $pos = strpos($buffer, "\n");
        // 改行文字がない場合、パケット長が不明なため、0を返してデータを待ち続ける
        if($pos === false)
        {
            return 0;
        }
        // 改行文字がある場合、現在のパケット長(改行文字を含む)を返す
        return $pos+1;
    }

    /**
     * パッケージ化する。クライアントにデータを送信する際に自動的に呼び出される
     * @param string $buffer
     * @return string
     */
    public static function encode($buffer)
    {
        // JSONシリアライズし、改行文字を付加してリクエストの終了を示す
        return json_encode($buffer)."\n";
    }

    /**
     * 解凍する。受信したデータのバイト数がinputが返す値(0より大きい値)と等しい時に自動的に呼び出され
     * onMessageコールバック関数の$data引数に渡される
     * @param string $buffer
     * @return string
     */
    public static function decode($buffer)
    {
        // 改行を削除し、配列に戻す
        return json_decode(trim($buffer), true);
    }
}

これで、JsonNLプロトコルの実装が完了し、MyAppプロジェクトで使用可能です。使用方法の例は以下の通りです。

ファイル:MyApp\start.php

use Workerman\Worker;
use Workerman\Connection\TcpConnection;
require_once __DIR__ . '/vendor/autoload.php';

$json_worker = new Worker('JsonNL://0.0.0.0:1234');
$json_worker->onMessage = function(TcpConnection $connection, $data) {

    // $dataはクライアントから送られてきたデータで、すでにJsonNL::decodeにより処理されています
    echo $data;

    // $connection->sendで送信するデータは自動的にJsonNL::encodeメソッドでパッケージ化された後、クライアントに送信されます
    $connection->send(array('code'=>0, 'msg'=>'ok'));

};
Worker::runAll();
...

ヒント
workermanはProtocols名前空間内のプロトコルのロードを試みます。例えば、new Worker('JsonNL://0.0.0.0:1234')Protocols\JsonNLプロトコルのロードを試みます。
Class 'Protocols\JsonNL' not foundエラーが発生した場合は、オートローディングの実装を参照してください。

プロトコルインターフェースの説明

Workermanで開発したプロトコルクラスは、input、encode、decodeの三つの静的メソッドを実装する必要があります。プロトコルインターフェースの説明はWorkerman/Protocols/ProtocolInterface.phpに記載されており、以下のように定義されています。

namespace Workerman\Protocols;

use \Workerman\Connection\ConnectionInterface;

/**
 * プロトコルインターフェース
* @author walkor <walkor@workerman.net>
 */
interface ProtocolInterface
{
    /**
     * 受信したrecv_buffer内でパケットを分割するために使用
     *
     * $recv_buffer内でリクエストパケットの長さを取得できる場合は、全パケットの長さを返す
     * そうでない場合は0を返し、現在のリクエストパケットの長さを取得するために追加のデータが必要であることを示す
     * -1が返された場合は、リクエストが無効であることを示し、接続は切断される
     *
     * @param ConnectionInterface $connection
     * @param string $recv_buffer
     * @return int
     */
    public static function input($recv_buffer, ConnectionInterface $connection);

    /**
     * リクエストの解凍に使用
     *
     * inputの返り値が0より大きく、Workermanが十分なデータを受信した場合、自動的にdecodeが呼び出される
     * そしてonMessageコールバックをトリガーし、decodeしたデータをonMessageコールバックの第二引数に渡す
     * 完全なクライアントリクエストを受信した際に自動的にdecodeが呼び出されるため、ビジネスコード内で手動で呼び出す必要はない
     * @param ConnectionInterface $connection
     * @param string $recv_buffer
     */
    public static function decode($recv_buffer, ConnectionInterface $connection);

    /**
     * リクエストのパッケージ化に使用
     *
     * クライアントにデータを送信する必要がある場合、$connection->send($data);が呼び出されると
     * 自動的に$dataがencodeを用いてパッケージ化され、プロトコルデータ形式に変換された後、クライアントに送信される
     * したがって、クライアントに送信されるデータは自動的にencodeでパッケージ化されるため、ビジネスコード内で手動で呼び出す必要はない
     * @param ConnectionInterface $connection
     * @param mixed $data
     */
    public static function encode($data, ConnectionInterface $connection);
}

注意:

Workermanでは、プロトコルクラスが必ずProtocolInterfaceを基に実装する必要はありません。実際には、クラスにinput、encode、decodeの三つの静的メソッドが含まれていればプロトコルクラスとして機能します。