Как настроить протокол

На самом деле создание собственного протокола — это довольно простая задача. Простой протокол обычно состоит из двух частей:

  • Идентификатор, который разделяет границы данных
  • Определение формата данных

Пример

Определение протокола

Предположим, что идентификатор, разделяющий границы данных, — это символ новой строки "\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 в фреймворк Workerman.
  2. После получения значения $length фреймворк Workerman проверяет, получены ли данные длины $length в текущем буфере данных. Если нет, он будет продолжать ждать данные, пока длина данных в буфере не станет не меньше $length.
  3. Как только длина данных в буфере достаточна, workerman извлечет из буфера данные длины $length (т.е. разобьет на пакеты) и вызовет метод протокола decode для распаковки, в результате чего полученные данные будут $data.
  4. После распаковки workerman передаст данные $data в виде обратного вызова onMessage($connection, $data) бизнес-логике, которая в onMessage сможет использовать переменную $data для доступа к полностью распакованным данным от клиента.
  5. Когда в бизнес-логике необходимо отправить данные клиенту с помощью вызова метода $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)
     * И передает параметры $data обратному вызову onMessage
     * @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, и декодированные данные передаются второму параметру обратного вызова 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.