Привет, Хабр!

Swoole — высокопроизводительной асинхронный и многопоточный фреймворк для PHP. Он отличается от традиционной модели PHP-FPM, предлагая асинхронный ввод-вывод и корутины, а также возможность работать с веб-сокетами и различными сетевыми протоколами непосредственно в PHP.

Установим

Swoole доступен как расширение PECL:

sudo apt update
sudo apt install php php-dev php-pear

После установки добавляемextension=openswoole.so в файл php.ini, чтобы PHP мог загружать Swoole при запуске.

Устанавливаем Swoole через Composer как зависимость проекта:

composer require openswoole/core

С Докером можно изолировать установку и эксперименты с Swoole. Можно юзать официальный образ PHP с предустановленным Swoole или настроить свой. Создаем dockerfile:

FROM php:7.4-cli
RUN pecl install openswoole && docker-php-ext-enable openswoole

Собираем образ:

docker build -t php-swoole .

Запускаем контейнер и монтируем проект внутрь контейнера для тестирования:

docker run -p 9501:9501 -v $(pwd):/app -w /app php-swoole php your-script.php

Основная архитектура сервера Swoole

Процесс Master является основным и отвечает за запуск и управление остальными процессами. Master-процесс создает реакторные потоки, которые работают с сетевыми соединениями. Он также инициирует Manager-процесс для управления воркерами. Master-процесс также обрабатывает сигналы от ОС и занимается общей организацией работы сервера.

Реакторные потоки, созданные внутри Master-процесса, обрабатывают сетевые соединения с клиентами. Они используют асинхронный ввод-вывод для приема и передачи данных, применяя модели на основе событийного цикла, такие как epoll или kqueue. Реакторные потоки принимают данные от клиентов и передают их в рабочие процессы (о них чуть ниже).

Процесс Manager процесс отвечает за управление жизненным циклом рабочих процессов и задач. Он отслеживает состояние рабочих процессов и перезапускает их в случае отказа или завершения работы.

Рабочие процессы запускаются Manager-процессом и выполняют основную бизнес-логику. Они получают запросы от реакторных потоков, обрабатывают их и возвращают результаты обратно. Каждый рабочий процесс может быть настроен на выполнение определенного типа задач. Код приложения разрабатывается для запуска в этих процессах.

Задачные процессы (task workers) получают задачи от рабочих процессов и обрабатывают их параллельно. Они используются для выполнения операций, которые могут блокировать или замедлять основной поток событий, таких как длительные вычисления или взаимодействие с внешними API. Рабочие процессы отправляют задачи с помощью функций task, taskwait, и taskWaitMulti.

Примерно так выглядит процесс обработки запросов:

  1. Клиент отправляет запрос, который принимается реакторным потоком Master-процесса.

  2. Реакторный поток передает данные в рабочий процесс для выполнения основной бизнес-логики.

  3. Рабочий процесс может обрабатывать запрос сам или делегировать выполнение задач задачным процессам.

  4. Задачный процесс возвращает результат в рабочий процесс, который затем отправляет данные обратно клиенту через реакторный поток.

Кратко про основной синтаксис

Инициализация сервера происходит через создание экземпляра OpenSwoole\HTTP\Server.

Сервер обрабатывает события через методы on, такие как start и request.

Для отправки ответа используется метод end объекта Response.

Пример инициализации HTTP сервера:

$server = new OpenSwoole\HTTP\Server("127.0.0.1", 9501);
$server->on("request", function ($request, $response) {
    $response->end("Hello World\n");
});
$server->start();

Swoole поддерживает написание асинхронного кода через корутины. Методы go или co::run, используются для запуска корутин.

WebSocket сервер настраивается аналогично HTTP серверу с использованием событий open, message и close.

Swoole позволяет организовывать асинхронную обработку задач через Task Workers.

Для распределения задач используется метод task, а обработка завершения задачи выполняется через finish.

Пример настройки Task Workers:

$server->set(['task_worker_num' => 4]);
$server->on('task', function ($server, $taskId, $data) {
    // обработка задачи
});
$server->on('finish', function ($server, $taskId, $returnValue) {
    // завершение задачи
});

Swoole предоставляет классы для создания асинхронных TCP, UDP, HTTP клиентов.

Реализация TCP/UDP клиентов и серверов на Swoole

Для создания TCP/UDP сервера в Swoole используется класс OpenSwoole\Server. Сервер настраивается через метод set, где можно задать различные параметры, например - количество рабочих процессов. События сервера обрабатываются через метод on, который позволяет реагировать на различные события:

$server = new OpenSwoole\Server("127.0.0.1", 9501);
$server->set(['worker_num' => 4]);
$server->on('receive', function ($server, $fd, $reactor_id, $data) {
    $server->send($fd, 'Received: '.$data);
    $server->close($fd);
});
$server->start();

Swoole предоставляет различные классы для создания TCP и UDP клиентов. Например, OpenSwoole\Coroutine\Client позволяет создать асинхронного клиента, который может коннектится, отправлять и получать данные в неблокирующем режиме, используя корутины:

use OpenSwoole\Coroutine\Client;

co::run(function() {
    $client = new Client(SWOOLE_SOCK_TCP);
    if (!$client->connect('127.0.0.1', 9501, 0.5)) {
        echo "Connection failed: {$client->errCode}\n";
    }
    $client->send("Hello World\n");
    echo $client->recv();
    $client->close();
});

Обработка HTTP и WebSocket запросов с использованием корутин

Swoole предоставляет класс OpenSwoole\HTTP\Server для создания HTTP сервера. Можно обрабатывать HTTP запросы, используя корутины, что позволяет управлять множеством запросов параллельно без блокировки и с минимальными задержками.

Пример создания HTTP сервера:

use OpenSwoole\HTTP\Server;
use OpenSwoole\Http\Request;
use OpenSwoole\Http\Response;

$server = new Server("0.0.0.0", 9501);
$server->on("request", function (Request $request, Response $response) {
    // Обработка запроса
    $response->end("Hello, Swoole HTTP!");
});
$server->start();

Сервер использует событийную модель, где событие request активируется каждый раз, когда сервер получает новый HTTP запрос. Корутины позволяют обрабатывать асинхронные операции.

WebSocket сервер в Swoole обеспечивает полнодуплексное общение по TCP соединению. Пример создания WebSocket сервера:

use OpenSwoole\WebSocket\Server;
use OpenSwoole\Http\Request;
use OpenSwoole\WebSocket\Frame;

$server = new Server("0.0.0.0", 9502);
$server->on("open", function (Server $server, Request $request) {
    echo "Новое соединение: {$request->fd}\n";
});
$server->on("message", function (Server $server, Frame $frame) {
    echo "Получено сообщение: {$frame->data}\n";
    $server->push($frame->fd, "Эхо: {$frame->data}");
});
$server->on("close", function ($ser, $fd) {
    echo "Соединение закрыто: {$fd}\n";
});
$server->start();

Событие open активируется при подключении нового клиента, message — когда сервер получает сообщение от клиента, и close — когда клиент закрывает соединение.

Пример

Хороший пример работы с Swolle - это создание WebSocket сервера с механизмами для улучшенной безопасности и функциональности, включая маршрутизацию запросов и использование корутин:

<?php

use OpenSwoole\WebSocket\Server as WebSocketServer;
use OpenSwoole\Http\Request;
use OpenSwoole\WebSocket\Frame;

// маршруты для WebSocket запросов
$routes = [
    '/chat' => 'handleChat',
    '/notify' => 'handleNotifications',
];

// проверка допустимости маршрута
function checkRoute(string $path): bool {
    global $routes;
    return array_key_exists($path, $routes);
}

// обработчик чата
function handleChat($server, $frame) {
    // простая логика для отправки ответа клиенту
    $server->push($frame->fd, "Чат: {$frame->data}");
}

// обработчик уведомлений
function handleNotifications($server, $frame) {
    // пример: отправка уведомления всем подключенным клиентам
    foreach ($server->connections as $fd) {
        if ($server->isEstablished($fd)) {
            $server->push($fd, "Уведомление: {$frame->data}");
        }
    }
}

// создание WebSocket сервера
$server = new WebSocketServer("0.0.0.0", 9502);

// событие запуска сервера
$server->on("start", function(WebSocketServer $server) {
    echo "Сервер запущен на ws://0.0.0.0:9502\n";
});

// установка SSL для безопасности
$server->set([
    'ssl_cert_file' => '/path/to/your/cert.pem',
    'ssl_key_file' => '/path/to/your/key.pem',
    'open_http2_protocol' => true,
    'worker_num' => 4,
    'daemonize' => false,
]);

// установка события подключения
$server->on("open", function (WebSocketServer $server, Request $request) {
    echo "Новое подключение от клиента: {$request->fd}\n";
    // проверка допустимого маршрута
    if (!checkRoute($request->server['request_uri'])) {
        $server->disconnect($request->fd, 400, "Неверный маршрут");
    }
});

// обработка сообщений от клиентов
$server->on("message", function (WebSocketServer $server, Frame $frame) {
    // в этом примере предполагается, что первый символ данных — идентификатор маршрута
    global $routes;
    $routeKey = '/'.trim($frame->data);
    if (isset($routes[$routeKey])) {
        call_user_func($routes[$routeKey], $server, $frame);
    } else {
        $server->push($frame->fd, "Неверный маршрут");
    }
});

// событие закрытия соединения
$server->on("close", function ($ser, $fd) {
    echo "Соединение закрыто: {$fd}\n";
});

$server->start();

А теперь попробуем создать полнофункциональный HTTP-сервер:

<?php
use Swoole\Http\Server;
use Swoole\Http\Request;
use Swoole\Http\Response;

$server = new Server("0.0.0.0", 9501);

// настройки сервера
$server->set([
    'worker_num' => 4,  // колво рабочих процессов
    'ssl_cert_file' => '/path/to/ssl_cert.pem',
    'ssl_key_file' => '/path/to/ssl_key.pem',
]);

// маршруты
$routes = [
    '/login' => function ($request, $response) {
        // логика аутентификации
        $response->end(json_encode(['status' => 'success', 'message' => 'Logged in']));
    },
    '/data' => function ($request, $response) {
        // защищенный маршрут, требующий аутентификации
        $response->end(json_encode(['data' => 'secret data']));
    }
];

// обработчик запросов
$server->on("request", function (Request $request, Response $response) use ($routes) {
    $path = $request->server['request_uri'];
    $method = $request->server['request_method'];

    // валидация HTTP метода
    if ($method !== 'GET' && $method !== 'POST') {
        $response->status(405);
        $response->end();
        return;
    }

    // проверка наличия маршрута
    if (isset($routes[$path])) {
        $routes[$path]($request, $response);
    } else {
        $response->status(404);
        $response->end("Not found");
    }
});

$server->start();

Подробнее с Swolle можно ознакомиться в документации.

В заключение напоминаю про открытые уроки по PHP:

  • 14 мая: Сложные логические операции в PHP — детально разберём в игровой форме сложные логические операции. Записаться

  • 20 мая: Что нового в PHP 8.3? — посмотрим, что нового нам принесла новая минорная версия, и как это можно применять. Записаться

Комментарии (5)


  1. ponikrf
    14.05.2024 07:35

    Все это какие то костыли и подпорки для PHP. Представляю сколько гемора работать с этим без полноценных async await. Хотя о чем это я, это же просто реклама уроков по PHP.


  1. gruzoveek
    14.05.2024 07:35
    +3

    А обязательно писать так?

    global $routes;

    Нельзя например так?

    function (WebSocketServer $server, Frame $frame) use ($routes)


    1. gimcnuk
      14.05.2024 07:35

      _unexpected token "use"_

      только для анонимных функций


      1. FanatPHP
        14.05.2024 07:35
        +1

        @gruzoveekпишет про другое использование global, разумеется, как раз в анонимной функции.


    1. FanatPHP
      14.05.2024 07:35
      +3

      Это же badcasedaily1, ему тексты пишет GPT, а сам он не понимает ничего в этом коде. То же самое с динамическим вызовом функций: во втором примере нормально, $routes[$path]($request, $response);, а в первом откуда-то ископаемая call_user_func(). Если внимательно смотреть, то таких галлюцинаций по тексту много. Даже название перевирается постоянно.