Данная публикация не претендует на полноту решения поставленного вопроса. Сервер разрабатывается исключительно в ознакомительных целях. Многие важные вопросы, такие как, например, обработка ошибок сокетов, опущены. Для реализации многопоточного сервера мы будем использовать, конечно же, потоки. Очень часто приходится видеть фразу, что, мол, в 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

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


<?php

$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);

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));
        }
    }
}

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);
    }
}

Спасибо за внимание!
Поделиться с друзьями
-->

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


  1. webmasterx
    14.10.2016 11:22
    +3

    Но зачем?


    1. VovanZ
      14.10.2016 11:54
      +4

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


    1. shadovv76
      02.02.2017 09:42

      во всем этом решении так и хочется пропеть песню из мультфильма Фиксики: «Ба-та-рей-кааа»!
      к сожалению проблему отсутствия нуля в розетках квартир в сочетании со светодиодными лампами производителя решают как в этой песне.
      p.s. свет как правило нужен в темное время суток и обидно, если батарейки хранятся в этой же комнате.


      1. SerafimArts
        14.10.2016 14:32
        -3

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


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


        1. Gemorroj
          14.10.2016 15:50
          +3

          php-pm имеется ввиду вот это.
          другое дело, что над php демоном все равно нужно ставить nginx)


      1. romangoward
        14.10.2016 16:33
        +6

        Это, мягко говоря, не правда: master процесс в nginx при старте форкает воркеров, которые в асинхронном режиме обрабатывают соединения (по одному воркеру на поток ядра проца).

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

        Стабильнее и быстрее? Ну-ну.


        1. LionAlex
          17.10.2016 08:55

          Тут просто теплое с мягким. Естественно, стейтфул быстрее стейтлесс, т.к. не надо каждый раз поднимать все окружение (читать конфиг, строить роуты, коннектиться к базам и т.д.) для обработки одного запроса, но nginx тут ни при чем.


  1. begemot_sun
    14.10.2016 11:41
    -3

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


    1. begemot_sun
      15.10.2016 15:12
      +1

      Справедливости ради.

      Случай из практики.
      Обращается заказчик: Необходимо что-то сделать с php cервером который обрабатывает websocket-соединения.
      Сервер кое-как обслуживает 5 подключений, а потом все встает колом внутри него.
      Специфика протокола была такова, что клиенты генерировали очень много маленьких сообщений в единицу времени.

      PHP-сервер был выкинут, и на его место за 2 часа работы водружено нормальное решение, которое справляется с поставленными задачами до сих пор.


      1. Gemorroj
        15.10.2016 15:57

        случай из практики — нужен был websocket сервер. сделали на Ratchet. жрет 15-30 мб, обслуживает сотню одновременных подключений.


        1. begemot_sun
          15.10.2016 21:01

          случай из практики: жрет 70мб, обслуживает 10к соединений. :) кто больше?


          1. Gemorroj
            15.10.2016 21:33

            и все же
            >> 5 подключений, а потом все встает колом внутри него
            очевидно что тут дело совсем не в php. понятно, что интерпретируемый язык со сборщиком мусора по определению не самый производительный. но не до такой степени.


            1. begemot_sun
              15.10.2016 21:44

              Все зависит от задачи. PHP великолепен как генератор HTML.
              Но делать для него сервера, которые смогли бы обслуживать 10к и больше соединений, я бы не стал.
              Всему свой инструмент.


              1. baldrs
                16.10.2016 17:58

                У вас устаревший взгляд на PHP


                1. begemot_sun
                  16.10.2016 19:46
                  +1

                  Нет, скорее кто знает PHP пихает его везде где можно. Может быть достаточно попробовать другие инструменты? Может быть тогда вы измените свои взляды?


                  1. Fafhrd
                    17.10.2016 01:24
                    +1

                    Может быть достаточно попробовать другие инструменты?

                    В случае с «Сервер кое-как обслуживает 5 подключений» было незнание одного инструмента, потому начали использовать тот, который знали лучше.
                    К PHP, как всегда, это отношения не имеет.


                  1. sav1812
                    17.10.2016 08:08

                    Нет, скорее кто знает PHP пихает его везде где можно. Может быть достаточно попробовать другие инструменты? Может быть тогда вы измените свои взляды?

                    По-моему, ровно то же самое можно сказать и тем, «кто знает другие инструменты». :)

                    Человеку свойственно в первую очередь пользоваться тем инструментом, которым он владеет лучше.


                    1. begemot_sun
                      17.10.2016 08:20

                      Согласен при условии что этот инструмент работает как надо в тех условиях, которые ему уготованы.


                      1. VolCh
                        22.10.2016 11:00

                        В случае с PHP обычно проблемы не на его стороне, а на стороне кода. PHP давно перестал быть только генератором HTML.


                    1. VolCh
                      22.10.2016 10:59

                      И это не просто свойственно, это повышает эффективность решения реальных задач. Грубо, на PHP я подниму относительной простой веб-сокет сервер за день, на Node.js — за три дня, на Go или C — за месяц. Примерно в той же пропорции будет и скорость решения возникающих проблем, от багфиксов до изменившихся требований. Естественно, пока их можно решить в рамках выбранного стека.


  1. akubintsev
    14.10.2016 11:47

    В конкретно данном случае лучше будет работать однопоточный асинхронный сервер


  1. develop7
    14.10.2016 12:01

    бенчмарки?


    1. webmasterx
      14.10.2016 12:16
      +4

      зачем?


      1. develop7
        14.10.2016 12:55

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


        1. shadovv76
          02.02.2017 09:35

          я похоже на два шага.
          ремонт семь лет назад закончен.
          и автоматику света сделал год назад.
          управляется с кнопки на стене, цветных кнопок пульта ТВ (которые как правило в режиме просмотра ТВ не используются) и по сети.
          пожелание простое тем кто делает ремонт не нужно повсюду провода!
          во все выключатели заведите ноль (N), фаза как правило присутствует на разрыв.
          P.S. у меня его не было заведено, но я обошелся (собственное know now)!


          1. OnYourLips
            14.10.2016 14:50
            +7

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


  1. Wexter
    14.10.2016 14:47
    -2

    Зачем?


    1. beldeveloper
      14.10.2016 14:52
      +1

      Как минимум в статье можно подсмотреть пример использования потоков для сложных вычислений или асинхронных действий.


      1. YaakovTooth
        14.10.2016 15:47

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


        1. beldeveloper
          14.10.2016 15:52

          Думаю да. Я сейчас занимаюсь разработкой аналитической системы и, если бы платформа была не self-hosted типа, я бы, наверное, использовал потоки для распределения параллельных вычислений на несколько ядер процессора. Разве это не имеет смысл?


  1. vit1251
    14.10.2016 15:50

    Немного смущает только, что нужно дополнительно в PHP включать потоки.


    1. beldeveloper
      14.10.2016 15:57
      +2

      На самом деле потоки в PHP могут пригодиться только в узкоспециализированных приложениях и, я думаю, если это действительно нужно, установить расширение не будет проблемой.


      1. vit1251
        14.10.2016 18:10

        Я бы сказал немного иначе — во многих самостоятельных приложениях используют потоки, мьютесы, семафоры, а еще иногда события и блокировки. Именно из-за предметной области PHP в нем это в диковинку, а в любом другом языке потоки стандартное средство языка. И тут надо ответить себе на вопрос «Почему мне понадобились потоки?». Варианты ответа: 1. я не по назначению использую PHP 2. я не могу взять другой язык с потоками 3. автор реализации не предполагал написания самостоятельных приложений


        1. VolCh
          14.10.2016 23:37
          +1

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


          1. vit1251
            15.10.2016 01:43
            +1

            1. Официальные расширения — стандартное средство языка

            1.1. Открываю документацию и смотрю на список стандартных функций (built-in function) никаких упоминаний о потоках (в основном файлы и строки), а потоки относятся к _расширениям языка_ (PECL Extensions). При дальнейшем чтении становиться ясно, что механизмов расширений в PHP бывает несколько видов и каждое расширение нужно собрать и подключить… _но многие расширения_ уже включены в дистрибутив (но не подключены к языку — в случае с pthreads нужно это сделать самостоятельно). Следовательно это не стандартные средства языка, а расширенные с помощью расширений и нужно добиваться их появления:

            Also note that many extensions are enabled by default and that the PHP manual…

            Для примера берем потоки в Java и смотрим описание для пакета «java.lang» где находиться класс Thread:

            Package «java.lang» provides classes that are fundamental to the design of the Java programming language.

            Таким образом в Java потоки относятся к фундаментальному дизайну языка )

            2. PHP — язык программирования общего назначения, у него нет предметной области.

            2.1. Открываю страницу «Introducing — What is PHP?» и читаю

            Although PHP's development is focused on server-side scripting,…

            ( дальше конечно написано, что если очень нужно, то можно писать и всякое остальное, но это же не «focused» и понятное дело не «common-way», а всякой забавы ради не целевое использование полимеров )

            2.2. Открываю FAQ и читаю вопрос «What is PHP?»

            PHP is an HTML-embedded scripting language. The goal of the language is to allow web developers to write dynamically generated pages quickly.


  1. shushu
    14.10.2016 16:26

    Потоки в php и в придачу еще и под windows. Полный набор…

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


  1. amaksr
    14.10.2016 16:32
    +1

    Многие важные вопросы, такие как, например, обработка ошибок сокетов, опущены.

    Так ведь это самое интересное. Вам придется много-много страниц кода написать, пока у вас получится более-менее работающая реализация HTTP, и пока вы сможете начать писать бизнес-логику.
    Поэтому соглашусь с предыдущими комментаторами:
    Зачем?


  1. Tatikoma
    14.10.2016 16:52
    +2

    У меня две мысли.

    1. У меня сервера на Debian, включение thread safety в deb-пакете не предусмотрено (даже если пересобираем пакет с включением thread safety — оно не работает, какой-то из дебиановских патчей ломает), по крайней мере так было два года назад (тогда же пробовал pthreads — сегфолты на каждом шаге). Соответственно это нужно брать голый PHP и собирать из исходников…

    2. Никаких серьезных преимуществ у использования тредов перед процессами — нет, после создания сокета можно нафоркать несколько процессов, которые будут принимать соединения. Работать будет ничуть не хуже, но без необходимости включения страшных расширений.


    1. vit1251
      14.10.2016 18:16

      Ваша вторая мысль очень спорная. Преимущества начинаете понимать когда разберетесь в вопросе и используете и то и другое достаточно хорошо для того что бы «прочувствовать» разницу. Ну это как в Африке знают только один вид снега, а на северном полюсе всего один тип песка, но мы то знаем… снега и песка бывает много разных типов… так и отличий на самом деле огромное количество… взять хотя бы размер стека выделяемого под поток…


      1. Tatikoma
        14.10.2016 18:26

        Согласен, мысль спорная. Думаете стоит оформить в виде статьи с бенчмарками?


        1. vit1251
          14.10.2016 19:31

          Не только можно, но и нужно оформить — хотя бы только для собственного спокойствия. Вот только как Вы задачу выбирать будете для бенчмарков?


    1. SanGreel
      14.10.2016 19:00

      Собирать PHP сложно только первый раз)
      Как выше было сказано, в php разработчиками не часто используются треды.

      Если по первому пункту снова возникнет надобность тут полезная информация.


  1. shevmax
    15.10.2016 08:04

    Давно подобный механизм использую и он очень удобен в тестировании сетевого софта. Очень часто пишу сетевые приложения и необходимо тестировать всевозможные сбои в работе (+ безопасность протокола) и вот тут проще на php быстро накидать код теста, чем писать на Си.


  1. romy4
    15.10.2016 10:46

    Я бы использовал потоки в тяжёлых sql запросах для движка.


  1. lnroma
    15.10.2016 19:07

    // при попытке закрытия сокета я получаю ошибку zend_mm_heap currupted, поэтому эту часть в тестовом решении опускаю
    //socket_close($this->socket);

    Это же segfault, потоки или сокеты?


    1. beldeveloper
      15.10.2016 22:34
      +1

      В основном потоке сокет закрывается нормально, во второстепенном — с этой ошибкой. Если поможете победить проблему — буду признателен.


      1. LionAlex
        17.10.2016 09:02

        Сначала socket_shutdown, потом уже socket_close ?


        1. beldeveloper
          17.10.2016 13:53

          Не помогает. Shutdown выполняется, а на close все равно эта ошибка. Причем именно когда сокет обрабатывается в новом thread-е. Если же дождаться выполнения thread-а и закрыть сокет в основном потоке, то всё работает хорошо. Очевидно какой-то баг, потому как алгоритм работы с сокетами верный.


  1. olegl84
    17.10.2016 00:54

    я лично очень часто использую параллельные вычисления на php. но я просто запуская воркеры в нескольких консолях.