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

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? — посмотрим, что нового нам принесла новая минорная версия, и как это можно применять. Записаться