[Из песочницы] Реализация многопоточного сервера на PHP

Данная публикация не претендует на полноту решения поставленного вопроса. Сервер разрабатывается исключительно в ознакомительных целях. Многие важные вопросы, такие как, например, обработка ошибок сокетов, опущены. Для реализации многопоточного сервера мы будем использовать, конечно же, потоки. Очень часто приходится видеть фразу, что, мол, в PHP потоков нет. Так вот это неправда. Потоки есть, но реализованы в отдельном расширении pthreads.

Для начала нам понадобится сборка PHP, скомпилированная с флагом thread safety. Я использую Windows для работы, поэтому скачал готовый пакет здесь. Нужно лишь правильно выбрать разрядность ОС, нужную версию PHP и, конечно же, Thread Safe версию. На протяжении статьи будет предполагаться, что архив с PHP мы распаковали в C:\php директорию. Далее нам нужно установить расширение pthreads. Идем сюда и выбираем версию, соответствующую скачанной версии PHP и разрядности системы. Из архива копируем файл php_pthreads.dll в директорию C:\php\ext и файл pthreadVC2.dll в директории C:\php и C:\Windows\System32. В директории C:\php переименовываем файл php.ini-development в php.ini и добавляем в него такую строку:
extension=php_pthreads.dll

Также находим и раскоменчиваем директиву extension_dir и выставляем ей значение «C:\php\ext» (у меня в версии PHP7 относительные пути не заработали). Открываем командную строку и проверяем:
C:\php\php.exe -v

В конце первой строки вывода мы должны увидеть пометку (ZTS). Переходим непосредственно к реализации сервера. Создаем файл (в моём случае он будет располагаться по адресу C:\server.php. Для начала создадим сокет, который будет слушать порт 8080 на нашей локальной машине.
$server = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
socket_bind($server, '127.0.0.1', 8080);
socket_listen($server);

Далее создаём пул воркеров.
$pool = new Pool(10, Worker::class);

Первый аргумент устанавливает максимальное количество действующих потоков, второй имя класса воркера. Для каких либо более определенных задач можно описать свой класс, унаследовав его от класса Worker. Мы же будем использовать оригинальный класс. Забегая вперед скажу, что в классе потока установленный воркер можно получить через $this→worker.

Далее реализуем класс, который будет выполняться в отдельном потоке. Класс должен наследоваться от Threaded.

class Task extends Threaded
{
    protected $socket;

    public function __construct($socket)
    {
        $this->socket = $socket;
    }

    public function run()
    {
        if (!empty($this->socket)) {
            $response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 12\r\n\r\nHello world!";
            socket_write($this->socket, $response, strlen($response));
            // при попытке закрытия сокета я получаю ошибку zend_mm_heap currupted, поэтому эту часть в тестовом решении опускаю 
            //socket_close($this->socket);
        }
    }
}

Наш класс принимает в конструкторе сокет соединения с клиентом. Так же действия, выполняемые в потоке, должны быть описаны в методе run (). В моем случае это ответ клиенту базовых заголовков и текста «Hello world!».

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


$servers = [$server];

while (true) {
    $read = $servers;

    if (socket_select($read, $write, $except, 0) >= 0 && in_array($server, $read)) {
        $task = new Task(socket_accept($server));
        $pool->submit($task);
    }
}

Поскольку мы используем бесконечный цикл, я зарегистрирую функцию, которая выполнится при завершении работы скрипта и остановит работу пула. Функцию следует регистрировать до начала цикла.

register_shutdown_function(function () use ($server, $pool) {
    if (!empty($server)) {
        socket_close($server);
    }

    $pool->shutdown();
});

Собственно всё. Запускаем сервер в командной строке и пробуем открыть в браузере localhost:8080.
cd C:\
C:\php\php.exe server.php

Ниже привожу полный код сервера.

socket = $socket;
    }

    public function run()
    {
        if (!empty($this->socket)) {
            $response = "HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: 12\r\n\r\nHello world!";
            socket_write($this->socket, $response, strlen($response));
        }
    }
}

register_shutdown_function(function () use ($server, $pool) {
    if (!empty($server)) {
        socket_close($server);
    }

    $pool->shutdown();
});

$servers = [$server];

while (true) {
    $read = $servers;

    if (socket_select($read, $write, $except, 0) >= 0 && in_array($server, $read)) {
        $task = new Task(socket_accept($server));
        $pool->submit($task);
    }
}

Спасибо за внимание!

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

  • 14 октября 2016 в 11:22

    0

    Но зачем?
    • 14 октября 2016 в 11:54

      –1

      троллейбус_из_хлеба.jpg

    • 14 октября 2016 в 13:32

      –1

      Такие штука на несколько порядков быстрее и стабильнее серверов, вроде Nginx. На просторах хабра были где-то статьи на эту тему с бенчмарками, найти правда не могу.

      • 14 октября 2016 в 14:32

        0

        Минусы за то, что нет пруфов подозреваю? Ловите: https://habrahabr.ru/post/220393/


        Любая стейтфулл реализация сервера на пыхе будет на порядок быстрее и стабильнее стейлесс серверов. И не из-за того, что пых якобы быстрее сишек, а из-за модели работы. Это очевидно каждому понимающему модель работы пыха в классическом варианте.

  • 14 октября 2016 в 11:41

    +1

    Сколько перерезал, сколько душ я загубил.
    Сколько PHP-серверов многопоточных я убил.
  • 14 октября 2016 в 11:47

    0

    В конкретно данном случае лучше будет работать однопоточный асинхронный сервер
  • 14 октября 2016 в 12:01

    –1

    бенчмарки?

    • 14 октября 2016 в 12:16

      +3

      зачем?
      • 14 октября 2016 в 12:55

        0

        чтобы наглядно проиллюстрировать пределы применимости

        • 14 октября 2016 в 13:27 (комментарий был изменён)

          –1

          Ну для для продакшена лучше использовать ReactPHP или Kraken


          Бенчмарки некоторые

          https://habrastorage.org/getpro/habr/comment_images/b52/37a/6e4/b5237a6e4838671f1753127edd178a4f.png

          • 14 октября 2016 в 14:50

            0

            Добавьте в график жесткий диск Seagate Barracuda ST1000DM003: там 7200/мин.
            И болгарку Makita 9555 HN: уже целых 10000/мин. Лидер найден!
  • 14 октября 2016 в 14:47

    0

    Зачем?
    • 14 октября 2016 в 14:52

      0

      Как минимум в статье можно подсмотреть пример использования потоков для сложных вычислений или асинхронных действий.
      • 14 октября 2016 в 15:47

        0

        Потоки для сложных вычислений? :) Вы точно правильно понимаете, что такое треды? :)

© Habrahabr.ru