Делаем вебсокеты на PHP с нуля. Часть 2. IPC

    После написания моей предыдущей статьи Делаем вебсокеты на PHP с нуля я понял, что у сообщества есть некоторый интерес к поднятой мною теме.

    В прошлой статье я обещал, что опишу:
    • запуск нескольких процессов для обработки соединений
    • межпроцессное взаимодействие
    • разделение процессов мастер-воркер
    • проксирование вебсокетов с помощью nginx
    • запуск из консоли
    • интеграция с вашим фреймворком на примере yii
    • демонстрация


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

    Запуск нескольких процессов для обработки соединений


    Для работы простого сервера вебсокетов достаточно одного процесса, но чтобы увеличить количество одновременных соединений (и обойти ограничение 1024 одновременных соединения), а также для использования ресурсов всего процессора (а не только одного ядра), необходимо, чтобы сервер вебсокетов использовал несколько процессов (оптимально — количество процессов = количество ядер процессора).

    Для запуска нескольких процессов мы будем использовать функцию pcntl_fork(). Она создаёт новый процесс (дочерний), который является практически полной копией процесса-родителя, выполняющего этот вызов.
    После вызова pcntl_fork() алгоритм разветвляется: в случае успешного выполнения функции pcntl_fork() она возвращает PID дочернего процесса родительскому, а NULL дочернему. Если создание форка закончилось неудачей, функция pcntl_fork() возвращает значение −1).

    $pid = pcntl_fork(); //делаем форк
    
    //далее весь код будет выполняться в обоих процессах
    
    if ($pid == -1) {
        // Не удалось создать дочерний процесс
    } elseif ($pid) {
        // Этот код выполнится родительским процессом
    } else {
        // А этот код выполнится дочерним процессом, его PID можно узнать с помощью функции getmypid()
    }
    


    Про отличие родительского процесса от дочернего можно почитать на википедии.

    Мы можем в цикле создавать столько дочерних процессов, сколько нам необходимо:

    $childs = array();
    
    for ($i=0; $i<4; $i++) {
        $pid = pcntl_fork(); //создаём форк
    
        if ($pid == -1) {
            die("error: pcntl_fork");
        } elseif ($pid) { //родительский процесс
            $childs[] = $pid; //заполняем массив дочерними PID, они нам ещё пригодятся :)
        } else { //дочерний процесс
            break; //выходим из цикла, чтобы дочерние процессы создавались только из родителя
        }
    }
    


    Межпроцессное взаимодействие


    Для взаимодействия между родительским и дочерним процессом мы будем использовать сокеты, а именно связанные сокеты:
    Функция stream_socket_pair() создаёт пару связанных неразличимых потоковых сокетов. Таким образом мы можем писать в один сокет, а считывать данные из второго.

    $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); //получаем массив из связанных сокетов
    
    fwrite($pair[0], 'тест'); //пишем в первый сокет
    fread($pair[1], mb_strlen('тест')); //читаем из второго
    


    Теперь совмещаем этот код с форками и получаем:

    $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); //создаём связанные сокеты
    
    $pid = pcntl_fork(); //делаем форк
    
    //далее весь код будет выполняться в обоих процессах
    
    if ($pid == -1) {
        die("error: pcntl_fork");
    } elseif ($pid) { //родительский процесс
        fclose($pair[0]); //закрываем один из сокетов в родителе
        $child = $pair[1]; //второй будем использовать для связи с потомком
    } else { //дочерний процесс
        fclose($pair[1]); //закрываем второй из сокетов в потомке
        $parent = $pair[0]; //первый будем использовать для связи с родителем
    }
    


    Итоговый код для создания множества дочерних процессов:

    $parent = null;
    $childs = array();
    
    for ($i=0; $i<5; $i++) {
        $pair = stream_socket_pair(STREAM_PF_UNIX, STREAM_SOCK_STREAM, STREAM_IPPROTO_IP); //создаём связанные сокеты
    
        $pid = pcntl_fork(); //создаём форк
    
        if ($pid == -1) {
            die("error: pcntl_fork");
        } elseif ($pid) { //родительский процесс
            fclose($pair[0]); //закрываем один из сокетов в родителе
            $childs[] = $pair[1]; //второй будем использовать для связи с потомком
        } else { //дочерний процесс
            fclose($pair[1]); //закрываем второй из сокетов в потомке
            $parent = $pair[0]; //первый будем использовать для связи с родителем
            break; //выходим из цикла, чтобы дочерние процессы создавались только из родителя
        }
    }
    

    В результате работы этого кода в родителе массив $childs будет содержать в себе все сокеты для связи с потомками, а потомки для связи с родителем будут использовать $parent.

    Разделение процессов на мастера и воркеров


    Так как дочерние процессы в нашей реализации не связаны друг с другом напрямую и могут взаимодействовать только через родителя, то целесообразно разделение обязанностей между родителем и потомками:
    • родитель будет отвечать за взаимодействие между дочерними процессами (будет мастером)
    • дочерние процессы будут выполнять всю работу (будут воркерами)

    Также воркер у нас будет заниматься пересылкой сообщений из скриптов со страниц сайта или из крона. Для этого мы создадим дополнительный сокет, и добавим его в массив, прослушиваемых сокетов. Например, можно создать unix-сокет:
    $service = stream_socket_server('unix:///tmp/websocket.sock', $errorNumber, $errorString);
    


    Проксирование вебсокетов с помощью nginx


    Nginx поддерживает проксирование вебсокетов начиная с версии 1.3.13. Благодаря nginx можно обрабатывать соединения к серверу вебсокетов на том же порту, что и сайт, а также ограничить количество открытых вебсокетов с одного ip и другие полюбившиеся вам плюшки.

    Пример nginx-конфига, который это позволяет:
    limit_conn_zone $binary_remote_addr zone=perip:10m;
    
    server {
        listen 5.135.163.218:80;
        server_name sharoid.ru;
    
        location / {
            limit_conn perip 5; #делаем ограничение 5 вебсокетов на 1 ip
            proxy_pass http://127.0.0.1:8000;
            proxy_http_version 1.1;
            proxy_set_header Upgrade $http_upgrade;
            proxy_set_header Connection "upgrade";
            proxy_read_timeout 3600s; #увеличиваем таймаут для вебсокетов
        }
    }
    


    Запуск из консоли


    Выполняем команду php websocket.php или ./websocket.php (предварительно дав права на выполнение)
    Если использовать nohup, например, nohup ./websocket.php &, то скрипт продолжит работать после закрытия консоли.

    По-умолчанию есть два ограничения количества соединений на один процесс.
    • Первое — на уровне операционной системы: в одном процессе нельзя открыть более чем 1024 соединения. Чтобы обрабатывать больше одновременных соединений, выполните команду: ulimit -n 65535, а если у пользователя недостаточно привилегий, то sudo sh -c "ulimit -n 65535 && exec su $LOGNAME". Текущее значение можно посмотреть используя команду ulimit -n
    • Второе — у функции stream_select(): она не принимает больше чем 1024 соединения. Здесь всё сложнее — нужно перекомпилировать php c увеличенным FD_SETSIZE

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

    Интеграция с вашим фреймворком на примере yii


    Так как наш мастер прослушивает дополнительный сокет для связи с нашими скриптами (в примере выше был unix:///tmp/websocket.sock), мы можем в любом месте нашего сайта или в кроне соединиться с этим сокетом и отправить сообщение, которое мастер разошлёт всем воркерам, а они, в свою очередь, все клиентам:
    $service = stream_socket_client ('unix:///tmp/websocket.sock', $errno, $errstr);
    fwrite($service, 'всем привет');
    

    С использованием компонента yii это будет выглядеть вот так:
    Yii::app()->websocket->send('всем привет');
    

    Подробнее для yii
    Скачиваем экстеншн, кладём его в папку extensions/websocket
    В папку components кладём Websocket.php, WebsocketMasterHandler.php и WebsocketWorkerHandler.php из папки sample/yii.
    В папку commands кладём из WebsocketCommand.php из папки sample/yii.
    В конфиги main.php и console.php вставляем в секцию components:
    'websocket' => array(
        'class' => 'Websocket',
        //'websocket' => 'tcp://127.0.0.1:8000',
        //'localsocket' => 'tcp://127.0.0.1:8001',// unix:///tmp/mysock
        //'workers' => 1
    ),
    

    В конфиг console.php также вставляем в секцию import:
    'ext.websocket.*'
    



    Демонстрация


    Демонстрационный чат 2.0 (добавлен список пользователей, добавлено ограничение: 1 сообщение в секунду с одного IP)
    В нём были использованы описанные выше функции, а также исправлены недостатки, выявленные после публикации предыдущей статьи.
    Демонстрационный чат 1.0 (без списка пользователей, без ограничений)

    Все исходники я оформил в виде библиотеки и выложил на github

    Update: Если сообществу интересна эта тема, то следующая статья будет про то как сделать простую игру, в которой все участники будут находиться на одном игровом поле и взаимодействовать друг с другом в реальном времени (демка уже почти готова).

    Третья часть статьи: От чата до игры: Battle City
    i-Free Group
    49.79
    Company
    Support the author
    Share post

    Comments 15

      0
      Спасибо за статью. Мы используем в проекте Ratchet. Можете прокомментировать чем ваше решение отличается/лучше от уже устоявшейся, стабильной библиотеки?

      Спасибо
        0
        Отсутствует зависимость от установки libevent, отсюда простота использования, лёгкая интеграция в существующий проект.
          0
          Lib-event опциональная зависимость.
          Allows for use of a more performant event-loop implementation.


          Насколько я помню, react так же использует сокеты для коммуникации
            0
            Сокеты используют все. Возможно вы имели ввиду, что он использует select вместо libevent?
            Хорошо что у вас есть опыт использования ratchet. У меня к вам есть пара вопросов.
            Я не нашёл как для обработки вебсокетов использовать несколько процессов. Он это поддерживает?
            Я не нашёл как можно расширять мастер процесс. Он это поддерживает?

            В моём примере мастер отвечает за пересылку данных между воркерами, а также обработку сообщений из сторонних скриптов (например из существующего проекта).
              0
              Сокеты используют все. Возможно вы имели ввиду, что он использует select вместо libevent?

              Тут Вы правы, но я имел ввиду парные сокеты для коммуникации воркеров.

              Хорошо что у вас есть опыт использования ratchet. У меня к вам есть пара вопросов.

              Боюсь что я не настолько компетентен в этом вопросе, именно поэтому просил Вас сделать сравнение…
        +2
        что по лицензии? хочу применить плагин для yii в своем проекте
        0
        Просто интересно, а почему chat2.html?(вопрос с расширении html) а не php?
          0
          Это клиентская часть. php там лишний. К тому же клиент можно запустить локально и соединиться с сервером вебсокетов удалённо.
            0
            ок.Спс, об удаленном доступе как то не подумал.
          +2
          Если сообществу интересна эта тема, то следующая статья будет про то как сделать простую игру

          Конечно интересно, пишите ещё!
            +1
            for ($i=0; $i<5; $i++) {

            5 в вашем цикле — это количество ядер процессора?
              0
              Нет, 5 — это «столько дочерних процессов, сколько нам необходимо».
              Но для того чтобы не было разночтений поменял на 4.
                0
                Лучше сделать константу или переменную, а то и вообще чтение из конфига :)
                  0
                  Так это же кусок кода для демонстрации. В итоговой библиотеке (которая выложена на гитхаб) количество воркеров конечно же вынесено в конфиг.

            Only users with full accounts can post comments. Log in, please.