Производительный сетевой сервер на PHP

    Вы пробовали заказать в Макдональдсе жаренного на орудийном шомполе поросенка с домашним вином и, на десерт, девушку рядом с вами за столиком, для приятной беседы во время трапезы? Даже не думали об этом?? Вот-вот — статья как раз об этом, о стереотипах программиста и лени, двигающей прогресс. А если серьезно — в статье мы напишем очень полезный многим высокопроизводительный сетевой сервер на PHP за пару часов. Я совершенно серьезно :-)


    В старые добрые времена...


    В старые добрые времена, когда люди были ближе к природе, свежее пиво радовало приятной горчинкой и женщины изысканно пахли — программисты были ближе к «железу» и писали на С и, в моменты вдохновения — на ассемблере. Но наверно самое важное — программисты понимали, что такое tcp, чем оно отличается от udp, и как эффективно взаимодействовать с ядром операционной системы через интерфейс системных вызовов.

    Лень наступает...


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

    Люди стали создавать в программах объекты вымышленного мира, философские концепции, обменивающиеся сообщениями и все больше стали отрываться от реальности и природы. И если в C++ еще пытались задержаться наедине с природой через указатели и контролируемую работу с памятью, то в Java и C# лень взяла свое и программисты оказались в идеальной, но далеко не эффективной вселенной резиновых женщин и безалкогольного пива. Философия проиграла в создании универсального API для работы со всеми видами файловых систем или обязательной обработке исключений (Java).

    А сейчас даже по сторонам стало страшно смотреть: разработчики вообще «разленились» до такой степени, что не пользуются компиляторами :-) Многие системы создаются на слаботипизированных скриптовых языках типа Python/PHP — которые не только неплохо поддерживают ООП, но такое настолько мощны, что позволяют одной функцией эффективно загрузить файл в переменную :-)

    Процессоры и потоки


    Многие свято верили в аппаратную поддержку ООП на уровне процессора в 90-ые, этого так и не случилось. Но лень продолжает влиять и заставляет сейчас свято верить в эффективную реализацию потоков языка программирования — с учетом моды на размножение ядер процессоров. Т.е. не хочется напрягаться и стоит лишь написать «new Thread» и все заработает эффективно и быстро.

    А тем временем...


    А тем временем мир захватывают эффективные решения на C в стиле nginx, создаются «близкие к железу» производительные NoSQL решения и когда речь заходит о скорости, производительности — одержимый ленью и рекламой мозг начинает шевелиться и ощущать — что-то не то! «Врут» же про потоки — не работают они достаточно эффективно, даже на мультиядерных железках. Хотя теоретически — должны!

    Забыли истоки...


    Спроси сейчас у разработчика о разнице close и shutdown или об отличиях процесса от потока… и все чаще и чаще видишь, что значит «экспрессивно грызть ногти» :-) А ведь чтобы написать полезный сервер, нужно хорошо понимать, как устроена операционная система, какие бывают сетевые протоколы, как вообще устроена природа вещей и что такое настоящее пиво! :-)

    Дело не в языке программирования


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

    Обработка соединений


    Как эффективно обрабатывать сетевые сокеты люди, когда были ближе к природе, знали. Конечно, этим должно заниматься ядро операционной системы и уведомлять вас о наступлении события:
    1) Пришел новый сокет в слушающем соединении (listen) — и его можно взять в обработку (accept)
    2) Можно прочитать из сокета, не блокируя процесс (read).
    3) Можно записать в сокет, не блокируя процесс (write).

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

    Для меня до сих пор остается загадкой, почему, несмотря на появление в той же java около 10 лет назад библиотеки для решения серверных задач «в стиле nginx» — она так и не получила должного распространения и приложения продолжают «фигачить» на потоках, несмотря на всю тупиковость и расточительность такого подхода! :-)

    А почему все так не делают?


    Просто лень :-) Хотя также считается, что асинхронная обработка демультипрексированных сокетов — это гораздо сложнее с точки зрения программирования, чем 50 строк в отдельном процессе. Но ниже я покажу, как даже на заточенном немного на другие задачи PHP написать аналогичный сервер — совсем просто.

    PHP и сокеты


    В PHP есть поддержка «родных дорогих» BSD-сокетов. Но это расширение, к большому сожалению, не поддерживает ssl/tls.
    Поэтому нужно лезть в немного отстраненный от природы и здорового образа жизни, наполненный абстракциями, «гоблинами и некроморфами» интерфейс потоков — streams. Если взять лопату и отбросить кучу шелухи, за этим интерфейсом можно увидеть сетевые сокеты и довольно эффективно с ними работать.

    Кусочки кода


    Я не буду приводить целиком исходный код сетевого сервера, а пройдусь по ключевым частям кода. В целом же, сервер устойчиво держит без перекомпиляции PHP до 1024 открытых сокета в одном процессе, занимая около 18-20МБ (это дофига по меркам C, но поверьте, бывают PHP-скрипты, кушающие гигабайты) и напрягая лишь одно ядро процессора (да, syscpu заметно больше, но как без него). Если пересобрать PHP, то select может работать с гораздо большим числом сокетов.

    Ядро сервера

    Задачи ядра сервера:
    1) Проверить массив заданий
    2) Для каждого задания создать соединение
    3) Проверить, что сокет в задании можно прочитать или записать без блокировки процесса
    4) Освободить ресурсы (сокет и т.п) задания
    5) Принять соединение от управляющего сокета на добавление задания — без блокировки процесса

    Говоря простыми словами, мы набиваем в ядро сервера задания на работу сокетами (например ходить по сайтам и собирать данные и т.п.) и ядро В ОДНОМ ПРОЦЕССЕ начинает шуровать сотни заданий одновременно.

    Задание

    Задание это объект в терминологии ООП типа FSM. Внутри объекта имеется стратегия — допустим: «зайди по этому адресу, создай запрос, загрузи ответ, распарси и т.п. возвраты в начало и в конце запиши результат в NoSQL». Т.е. можно создать задание от простой загрузки содержимого, до сложной цепочки нагрузочного тестирования с многочисленными ветвлениями — и это все, напоминаю, живет в объекте задания.
    Задания в данной реализации ставятся через отдельный управляющий сокет на 8000 порту — пишутся json-объекты в tcp-сокет и затем начинают свое движение в сервером ядре.

    Принцип обработки заданий и сокетов

    Главное — не позволить серверному процессу заблокироваться в ожидании ответа в функции при чтении или записи информации в сетевой сокет, при ожидании нового соединения на управляющий сокет или где-нибудь в сложных вычислениях/циклах. Поэтому все сокеты заданий проверяются в системном вызове select и ядро ОС уведомляет нас лишь тогда, когда событие случается (либо по таймауту).
            while (true) {
    
                $ar_read = null;
                $ar_write = null;
                $ar_ex = null;
    
                //Собираемся читать также управляющий сокет, вместе с сокетами заданий
                $ar_read[] = $this->controlSocket;
    
                foreach ($this->jobs as $job) {
    
                    //job cleanup
                    if ( $job->isFinished() ) {
    
                        $key = array_search($job, $this->jobs);
    
                        if (is_resource($job->getSocket())) {
                            //"надежно" закрываем сокет
                            stream_socket_shutdown($job->getSocket(),STREAM_SHUT_RDWR);
                            fclose($job->getSocket());
                        }
    
                        unset($this->jobs[$key]);
                        $this->jobsFinished++;
    
                        continue;
                    }
    
                    //Задания могут "засыпать" на определенное время, например при ошибке удаленного сервера  
                    if ($job->isSleeping()) continue;
    
                    //Заданию нужно инициировать соединение
                    if ($job->getStatus()=='DO_REQUEST') {
    
                        $socket = $this->createJobSocket($job);
                        if ($socket) {
                            $ar_write[] = $socket;
                        }
                    
                    //Задание хочет прочитать ответ из сокета
                    } else if ($job->getStatus()=='READ_ANSWER') {
    
                        $socket = $job->getSocket();
                        if ($socket) {
                            $ar_read[] = $socket;
                        }
                    
                    //Заданию нужно записать запрос в сокет
                    } else if ( $job->getStatus()=='WRITE_REQUEST' ) {
    
                        $socket = $job->getSocket();
                        if ($socket) {
                            $ar_write[] = $socket;
                        }
    
                    }
                }
    
                //Ждем когда ядро ОС нас уведомит о событии или делаем дежурную итерацию раз в 30 сек
                $num = stream_select($ar_read, $ar_write, $ar_ex, 30);
    

    Далее, когда событие произошло и ОС уведомила нас — начинаем в неблокирующем режиме обработку сокетов. Да, можно еще немного оптимизировать обход массива заданий, индексировать задания по номеру сокета и выиграть 10мс — но пока ..., точно угадали, лень :-)
                    if (is_array($ar_write)) {
                        foreach ($ar_write as $write_ready_socket) {
                            foreach ($this->getJobs() as $job) {
                                if  ($write_ready_socket == $job->getSocket()) {
                                    $dataToWrite = $job->readyDataWriteEvent();
                                    $count = fwrite($write_ready_socket , $dataToWrite, 1024);
                                    
                                    //Сообщаем объекту сколько байт удалось записать в сокет
                                    $job->dataWrittenEvent($count);
                                }
                            }
                        }
                    }
    
                    if (is_array($ar_read)) {
                        foreach ($ar_read as $read_ready_socket) {
    
                            ///// command processing
                            ///
    
                            //Пришло соединение на управляющий сокет, обрабатываем команду
                            if ($read_ready_socket == $this->controlSocket) {
                                $csocket = stream_socket_accept($this->controlSocket);
                                
                                 //Тут упрощение - верим локальному клиенту, что он закроет соединение. Иначе ставьте таймаут.
                                 if ($csocket) {
                                    $req = '';
                                    while ( ($data = fread($csocket,10000)) !== '' ) {
                                        $req .= $data;
                                    }
    
                                    //Обрабатываем команду также в неблокирующем режиме                                
                                    $this->processCommand(trim($req), $csocket);
                                    stream_socket_shutdown($csocket, STREAM_SHUT_RDWR);
                                    fclose($csocket);
                                }
    
                                continue;
    
                                ///
                                /////
    
                            } else {
    
                                //Читаем из готового к чтению сокета без блокировки 
                                $data = fread($read_ready_socket , 10000);
    
                                foreach ($this->getJobs() as $job) {
    
                                    if  ($read_ready_socket == $job->getSocket()) {
                                        
                                        //Передаем заданию считанные данные. Если сокет закроется, считаем пустую строку.
                                        $job->readyDataReadEvent($data);
    
                                    }
                                }
                            }
                        }
                    }
                }
    

    Сам сокет инициируется также в неблокирующем режиме, важно установить флаги, причем оба! STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT:
        private function createJobSocket(BxRequestJob $job) {
    
            //Check job protocol
    
            if ($job->getSsl()) {
            //https
    
                $ctx = stream_context_create(
                            array('ssl' =>
                                array(
                                    'verify_peer' => false,
                                    'allow_self_signed' => true
                                )
                            )
                );
    
                $errno = 0;
                $errorString = '';
    
                //Вот тут происходит временами блокировочка в 30-60мс, видимо из-за установки TCP-соединения с удаленным хостом, надо глянуть в исходники, но снова ... лень
                $socket = stream_socket_client('ssl://'.$job->getConnectServer().':443',$errno,$errorString,30,STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT,$ctx);
    
                if ($socket === false) {
                    $this->log(__METHOD__." connect error: ". $job->getConnectServer()." ". $job->getSsl() ."$errno $errorString");
                    $job->connectedSocketEvent(false);
                    $this->connectsFailed++;
                    return false;
                } else {
    
                    $job->connectedSocketEvent($socket);
                    $this->connectsCreated++;
                    return $socket;
                }
    
            } else {
            //http
    ...
    

    Ну и посмотрим в код самого задания — оно должно уметь работать с частичными ответами/запросами. Для начала сообщим ядру сервера что мы хотим записать в сокет.
    //Формируем тело следующего запроса
    
        function readyDataWriteEvent() {
    
            if (!$this->dataToWrite) {
    
                if ($this->getParams()) {
                    $str = http_build_query($this->getParams());
    
                    $headers = $this->getRequestMethod()." ".$this->getUri()." HTTP/1.0\r\nHost: ".$this->getConnectServer()."\r\n".
                        "Content-type: application/x-www-form-urlencoded\r\n".
                        "Content-Length:".strlen($str)."\r\n\r\n";
                    $this->dataToWrite = $headers;
                    $this->dataToWrite .= $str;
    
                } else {
                    $headers = $this->getRequestMethod()." ".$this->getUri()." HTTP/1.0\r\nHost: ".$this->getConnectServer()."\r\n\r\n";
                    $this->dataToWrite = $headers;
                }
    
                return $this->dataToWrite;
    
            } else {
                return $this->dataToWrite;
            }
    
        }
    

    Теперь пишем запрос, определяя сколько еще осталось.
    //Пишем запрос в сокет до того момента, когда полностью запишем его тело
        function dataWrittenEvent($count) {
    
            if ($count === false ) {
    
                //socket was reset
                $this->jobFinished = true;
    
            } else {
    
                $dataTotalSize = strlen($this->dataToWrite);
                if ($count<$dataTotalSize) {
                    $this->dataToWrite = substr($this->dataToWrite,$count);
                    $this->setStatus('WRITE_REQUEST');
                } else {
                    //Когда успешно записали запрос в сокет, переходим в режим чтения ответа
                    $this->setStatus('READ_ANSWER');
                }
    
            }
    
        }
    

    После получения запроса, читаем ответ. Важно понять, когда ответ прочитан полностью. Возможно вам понадобиться установить таймаут на чтение — мне это не потребовалось.
    //Читаем из сокета до момента, когда полностью прочитаем ответ и совершаем переход в другой статус
        function readyDataReadEvent($data)
        {
    
            ////////// Successfull data read
            /////
    
            if ($data)  {
                $this->body .= $data;
    
                $this->setStatus('READ_ANSWER');
    
                $this->bytesRead += strlen($data);
            /////
            //////////
    
            } else {
    
            ////////// А тут мы уже считали ответ и начинаем его парсить
            /////
    
                    ////////// redirect
                    if ( preg_match("|\r\nlocation:(.*)\r\n|i",$this->body, $ar_matches) ) {
    
                        $url = parse_url(trim($ar_matches[1]));
                        $this->setStatus('DO_REQUEST');
    
                    } else if (...) {
                        
                        //Так мы сигнализируем ядру сервера, что задание нужно завершить
                        $this->jobFinished = true;
                        ...
                    } else if (...) {
    
                        $this->setSleepTo(time()+$this->sleepInterval);
                        $this->sleepInterval *=2;
                        $this->retryCount--;
    
                        $this->setStatus('DO_REQUEST');
                    }
    
                $this->body = '';
                ...
    

    В последнем фрагменте мы можем иерархически направлять FSM по заложенной стратегии, реализуя различные варианты работы задания.
    В момент написания класса задания не покидало ощущение, что пишешь плагин для nginx ;-)

    Результат


    Вот видите, как просто и лаконично удалось решить задачу одновременной работы с сотнями и тысячами заданий и сокетов всего в одном процессе PHP. А представьте, если мы поднимем для данного сервера сколько процессов PHP, сколько ядер на сервере — да, это тысячи обслуживаемых клиентов. И тут нет огорода с потоками и неэффективным переключением контекста процессора и повышенных требований к памяти. Серверный процесс PHP потребляет всего около 20МБ памяти, а работает как лошадь :-)

    Итоги


    Понимая, какую пользу нам может принести ядро операционной системы для эффективной обработки сетевых сокетов — мы подстроились под нее и реализовали высокопроизводительный сервер на PHP — обслуживающий сотни открытых сетевых сокетов в одном процессе. При необходимости, можно перекомпилировать PHP и обрабатывать тысячи сокетов в одном процессе.
    Расширяйте круг знаний, не ленитесь под гнетом стереотипов — используя даже скриптовые слаботипизированные языки можно делать производительные серверы — главное знать как и не бояться экспериментировать :-) Всем удачи!
    1С-Битрикс 65,45
    Компания
    Поделиться публикацией
    Комментарии 78
    • +3
      Написано здорово!
      Однако, асинхронная работа с сетью не всегда простая и очевидная. Для кого-то (например меня) больше применимы микропотоки. Я, например, написал веб-сервер на python с использованием greenlet + gevent. Выглядит как поток, работает как поток, а поток не создается! Код остается легкочитаем, но, в то же время, реальные потоки не создаются.
      Для особо ленивых есть monkey patching — пишите обычное поточное приложение, затем применяете monkey patch, и, хоп, потоки больше не создаются! Магия просто.
      • +1
        Вообще интересно, когда же появятся эффективные потоки, которые раскидываются по железным процессорам? :-)
        • +1
          Они уже и так есть — проблема потоков в дорогом переключении контекста CPU (надо сохранить регистры и т.д.)
          Легковесные потоки (то есть симуляция потоков на уровне runtime) сталкивается с не всегда оптимальным размером кванта времени для потока — на что runtime повлиять без патчей ядра ОС никак не может.

          Есть ОС, в которых green threads реализованы на уровне ядра — но это все экспериментальные ОС, где используется один единственный runtime, который гарантирует, что green threads не поломают друг другу контексты.
          • +1
            Хочется чтобы потоки начали поддерживать не на уровне ОС, а на уровне железки, связывающей процессоры проводами и микросхемами:
            — сохранение контекста и регистров
            — переключение
            — шедулинг
            :-)
            • +1
              сталкивается с не всегда оптимальным размером кванта времени для потока

              А я говорил о кооперативной многозадачности, когда вы их сами переключаете. В greenlet они именно такие.
              • +1
                Во, классно. Но кто нить может захватить же поток и начать мешать другим?
                • +1
                  Да. Микропотоки не подходят для вычислений, а вот для сетевых приложений типа прокси, чтобы байты туда-сюда гонять — супер. Ну и monkey patching работает здорово: вы можете, по большому счету, использовать блокирующие функции, но они патчатся таким образом, что во время блокировки в одном микропотоке, могут работать другие микропотоки.
                  • +1
                    Как-то страшно :-) Обещали же эффективную работу сборщика мусора и jit — и где она, java иногда просто безобразно начинает залипать по неизвестным науке причинам. В C# видимо такая же беда — периодические конвульсии :-)

                    Там ниже synergy дал ссылочку где эрланг запускается вообще без операционки и быстро обслуживает сокеты, интересная темка, надо покопать.
              • 0
                Например, ядро Windows NT. Файберы уже тогда были. Совсем непопулярное ядро :)

                Еще одна тема, которой сто лет в обед, — IO Completion Ports. Кстати, тоже давно в ядре Windows NT.
              • 0
                Эффективное раскидывание легких потоков по процессорам есть в Go и специализированных средствах типа Erlang'а. Кооперативная многозадачность (на самом деле не совсем) позволяет сделать это достаточно эффективно.
            • +1
              Согласен, асинхронная работа требует предварительного массажа мозга. Зато выигрыш в производительности просто огромный. Интересный пример привели, про автотрансляцию кода потоков в код без потоков, спасибо.
              • 0
                Рекомендую посмотреть на PHPDaemon
                • +1
                  Я так понимаю, там используются сишные модули pecl для расширения PHP?
                  • +1
                    Ага, но оно того стоит :}
                    • +1
                      Тоже асинхронно мультиплексируете там или потоки эмулируете?
                      • +1
                        Мопед не мой, боюсь наврать — давно уже не приходилось.
                        Лучше позвать в топик TravisBickle.
                        • 0
                          Там ниже про эрланг пишут, которому ОСь не нужна и он еще круче чем select/pool работает с сокетами — интересно, может правда будущее за ним и нужно nginx срочно переписывать? :-)
                    • +1
                      Нет, не используются.
                      • +1
                        Ну как же, libevent. Да и runkit желателен
                        • +1
                          Был не прав, попутал с phpSocketDaemon от Chris Chabot.
                      • +1
                        если точнее libevent,
                        в данном посте автор показал, как самому реализовать libevent
                        • +1
                          точнее наверно работающее решение на php, использующее системный вызов select
                    • +4
                      Ох, зря вы упомянули MongoDB в контексте этой статьи :) Там всё очень плохо. На каждого клиента к mongos — 1 тред в mongos, 1 коннект mongos-mongod и ещё один тред в mongod, работа с сетью блокирующая. В итоге по 2-3 тысячи тредов на каждом mongod. Таски на мультиплексирование сети стоят аж с 2009 года.
                      • +1
                        Я много хорошего понаслылал про нее и поверил вот на слово :-) Уберу упоминание.
                        • +3
                          реклама — двигатель торговли (в данном случае продается поддержка и обучение)
                          если честно — там очень много подводных камней
                          • +1
                            Интересно, apache httpd решаться перейти на асинхронную модель когда нить? :-) Или будут жить в мире префорк-процессов и потоков.
                        • +1
                          Убрал про MongoDB от греха подальше ;-)
                        • 0
                          Спасибо, отличная статья!
                          • +1
                            «тысячи сокетов»… эрланг стоит в сторонке и тихо хихикает
                            • +4
                              На С я за 100 строк сделаю обработку сокетов лучше, чем эрланг, поверьте :-) Вот если бы эрланг заменил собой ядро операционной системы и предложил что-то более эффективное чем select/pool!
                              • +2
                                пожалуйста: erlangonxen.org/
                                • 0
                                  ух ты, и правда круто, своя операционка какая-то у них прямо
                                  • 0
                                    реализована на коротинах
                                    • 0
                                      Говорят сыроватая и tcp-стек не очень и его хотят на эрланге переписать? Интересна конечно идея отказаться от ОС общего назначения, но линукс сколько лет писали и десятки миллионов строк кода.
                              • +1
                                А вот нет: stackoverflow.com/questions/3536459/erlang-c-and-erlang
                                Эрланг как и java использует виртуальную машину и соответствующие рунтайм-издержки неизбежны. Плюс — он слаботипизированный, отсюда затраты времени на обработку типов в рунтайме.

                                Т.е. скорее всего один процесс работающий с select/pool на С будет обрабатывать столько же сокетов, сколько эрланг на одном аппаратном сервере с N-процессорами. Просто на C это писать долго, а на эрланге — быстро и можно железом решить задачу, что есть путь, далекий от природы и здорового образа жизни :-)
                                • 0
                                  Ну, это всё не мешает писать на Erlang видеостриминогвый сервер с 10Гб/с на одной машине www.highload.ru/2013/abstracts/1262.html

                                  Опять же, вопрос в том, как именно нужно «обрабатывать»? Если считать с одного сокета и записать в другой то может и разницы нет.
                              • +3
                                Ваша статья — сплошной пиар мясоедения и потребления алкоголя, где обработка сокетов выглядит лишь заманухой, а не наоборот :)
                                • 0
                                  Ха ха :-) На самом деле хотелось разбавить код чем-то полезным и приятным :-)
                                  • +2
                                    Спасибо. Я всё время хотел узнать, как разрабатывают Битрикс. И тут кое-что приоткрылось…
                                    • 0
                                      Да ну, в статье лишь цветочки. Битрикс разрабатывают суровые балтийские викинги, какой там макдональдс — исключительно здоровый образ жизни, свежий воздух, чистая вода, домашнее пиво, экспертные знания современных технологий! :-)
                                      • +1
                                        у большинство РНРников отношение к Битриксу времён 2005г больше отрицательное…
                                        я конечно понимаю, что прогресс ушел вперед, но осадок говнокода остался
                                        • +1
                                          А я вот раньше с вдохновением смотрел на ZendFramework и другие навороченные ООПшные проекты на PHP — а сейчас пришел к тому, что на PHP будет работать быстрее, если писать в стиле старого доброго C и лишь при необходимости применять объекты, когда выхода нет и лаконично не получается решить. Нагромождение объектов как в ZF играет злую шутку в интерпретируемых языках с виртуальными машинами.
                                          • +1
                                            Работать-то будет быстрее (если не эмулировать ООП функциями), но будет ли быстрее разработка и внесение изменений?
                                • +2
                                  Хочется увидеть полный исходный код. И еще, мне кажется, что для решения Вашей задачи подошел бы reactphp.org/. Вот, например ивент-луп: github.com/reactphp/event-loop.
                                  • 0
                                    Остальной код должен быть очевиден. Готов неясные моменты прояснить.
                                    • 0
                                      Цельный код легче воспринимается, а эти кусочки нужно каждый раз в голове сшивать. Конечно, пихать все исходники в статью не стоит. Я надеялся на какой-нибудь github/bitbucket.

                                      Несколько вопросов:

                                      1. Этот код давно был написан? Я не вижу ни одного public/protected/private.
                                      2. Используется ли в коде $ar_ex?
                                      3. Что посоветуете почитать по теме?
                                      • 0
                                        1. Очень недавно, «strict OOP», но не стал все копировать. Рекомендую там где нужно и полезно — ООП, но без фанатизма конечно.
                                        2. Нет.
                                        3. Книжки по юниксу и сети, типа www.unpbook.com
                                      • 0
                                        в первом же фрагменте

                                        foreach ($this->jobs as $job) {
                                        


                                        в отрыве от контекста уже не понятно
                                        • 0
                                          Итерация по массиву заданий/запросов/клиентов — чего угодно. Задания ставятся в массив отдельно, например я их ставлю через отдельный управляющий сокет по tcp — пишу туда строку.
                                      • 0
                                        На что только люди не идут ради запихивания PHP куда не надо. Я люблю PHP, хотя он доверху набит тесно спрессованными друг с другом «особенностями»… но это же не повод теперь писать на нем все, что угодно. Хотя если бы Zend сделал php 6 чем-то вроде nodePHP, я был бы только рад.

                                        А так — я полностью поддерживаю любовь к мясу. К алкоголю не очень, а вот мясо — крутотень.
                                        • +1
                                          Ну сейчас PHP уже стал языком общего назначения, как и python. Почему бы не использовать его возможности, тем более, что результаты получаются отличные. На С такую штуку точно лез 5 писать :-)
                                          • +1
                                            я пишу сервера на Си, такую штуку напишу за день-два…
                                            libev в помощь — то вообще пара часов на написания любого сервиса (без реализации сложной логики)
                                            • 0
                                              Если не усложнять, возможно, согласен. У Стивенса в учебнике рабочий пример занимает пару страниц. Нужно правда с буферами, аналогично как в nginx, настроить удобную работу только. В PHP проще — строки :-)
                                      • 0
                                        У меня ощущение, что этот код можно было бы и на C++ написать с той же ясностью. Зачем здесь PHP?
                                        • 0
                                          просто для примера направления мышления :-)
                                          • 0
                                            хочется чтобы не тулили везде потоки, а посмотрел на задачу шире
                                            • +1
                                              В мире UNIX потоки совершенно не общеприняты (в отличие от Windows).

                                              Чаще дробят по процессам, — надежнее, да и скиллы другие.

                                              Хотя, по-хорошему, нужно проводить исследование, но думаю, я прав.
                                          • +1
                                            Не могли бы вы уточнить диаметр орудийного шомпола?
                                            • 0
                                              О, это исключительно дело вкуса. Если выбрать большой диаметр. поросенок будет выглядеть… ну не очень аппетитно и видимо состоять из 2 частей :-)
                                            • +1
                                              Был полностью согласен с автором до момента:
                                              … даже на заточенном немного на другие задачи PHP написать аналогичный сервер — совсем просто
                                              Но после этого возник: «А зачем? А зачем это делать на PHP» :) Не спешите минусовать, я свой, тоже кое что могу на PHP :) Но зачем делать такое на PHP? Этот язык не был заточен с самого начала на работу отличную от «обработать запрос, отдать ответ». Зачем делать на нем демонов и async networking? Что бы отгрести проблем?

                                              Если статья чисто академического плана, то нет вопросов. А если это руководство к действию, то… :)
                                              • +2
                                                Ну так если у вас все остальное на PHP, то зачем сразу усложнять задачу. Сейчас PHP хорошо развит, почему бы его не использовать если это возможно и получается я бы сказал очень все быстро и экономично.
                                                • 0
                                                  Ну так если у вас все остальное на PHP, то зачем сразу усложнять задачу.

                                                  Примерно так же сказал мой коллега в начале одного проекта (который на тот момент действительно был только на PHP) и сделал парсер входящих по сокету данных на PHP. Да получилось, но он падал с разной периодичностью из-за меморилик, забирая с собой все что крутилось на том же сервере :)

                                                  Код был написан вполне себе хорошо, ревью делали все программисты группы, но ничего подозрительного с точки зрения PHP не находили. Вывод был печальный, утечка была при непонятых стечениях обстоятельств в С коде функций сокетов или ХМЛ парсера. Проблема решилась только с помощью Pythonа.

                                                  Так что, если в проекте реально понадобилась что-то типа асинхронной обработки запросов, а все программисты в группе исключительно пэхапешники, то у вас проблемы.
                                                  • 0
                                                    Я ловил меморилик на PHP при использовании curl вместе с ssl. Отказался от curl и переписал на чистых сокетах. Скорее всего утечка была в xml-парсере, можно через gdb попробовать зайти и глянуть где.
                                                    • 0
                                                      Да на С можно такую штуку написать, просто лень, если на PHP утечек не замечаю и работает быстр.
                                                      • +1
                                                        А я кажется нашел компромисс между «громоздкостью» С/С++, «тормозами» Python и «дзен-буддизмом» Erlang для решения подобных задач. Это язык D, на второй день чтения книги Александреску у меня получилось на нем сделать асинхронный TCP эхосервер на libev, а на четвёртый выделить его в модуль и «свернуть колбеки» с помощью фидеров. В итоге получилось что-то типа:

                                                            void echo_handler(Stream stream)
                                                            {
                                                                stream.timeout = 5;
                                                                while (stream.active)
                                                                {
                                                                    try
                                                                        stream.write(stream.read());
                                                                    catch (Stream.TimeoutException e) {
                                                                        stream.close();
                                                                        writeln("Connection closed by timeout.");
                                                                    } catch (Stream.ClosedException e) {
                                                                        writeln("Connection closed.");
                                                                    }
                                                                }
                                                            }    
                                                        
                                                        
                                                            auto loop = new Loop();
                                                            auto listener = new TCPListener(loop, &echo_handler);
                                                        
                                                        • 0
                                                          Красота какая
                                                          • +1
                                                            Язык просто супер, и похоже уже стал достаточно стабилен для серьёзного использования.
                                                • +1
                                                  Итак битва начинается!
                                                  В углу ринга в красных шортах выступает… Streams!
                                                  Во втором углу ринга в синих шортах ему противостоит… Sockets!
                                                  Бой комментирует восьмикратный чемпион мира в легком весе… Libevent!
                                                  Раунды для Вас обявляет обворожительная… Pthreads!
                                                  Помощь в организации и проведении чемпионата заботливо оказал… EIO!
                                                  • 0
                                                    Не не, все проще, нафиг streams — сокеты, чисты родные любимые BSD-сокеты, против некроморфов и потоков :-)
                                                    • 0
                                                      сам-то в сырцы сокетов и стримов заглядывал?
                                                      • 0
                                                        пхпшных? а что там интересного может быть :-) А в ядре linux недавно искал кстати ответ на вопрос, может тебе интересно будет — как реализовано на уровне tcp закрытие сокета вызовом shutdown(SHUT_RD). С ходу не нашел, хотя по rfc такая конструкция смысла похоже не имеет :-)
                                                  • +4
                                                    Что-то жрать захотелось.
                                                    • +1
                                                      Даже в джава можно использовать правильный не блокирующий фреймворк, PlayFramework например.
                                                      And what about Node.js?
                                                      • 0
                                                        Тесты нужны, если можно написать, но работает в реальной жизни плохо, то писать действительно не надо.
                                                        • +1
                                                          можно долго спорить о том, надо оно или не очень, просто бывают ситуации, когда задача ставится определенным образом и надо сделать «вот так», а не иначе. А по теме, сам недавно писал подобное на php для работы с web-sockets. При запуске сервер кушал всего 632148 байта, на каждое соединение тратится еще примерно 120000 байт. работает очень шустро. планирую прикрутить к нему волшебную библиотеку pthreads и посмотреть на что еще способен этот язык…
                                                          • 0
                                                            Спасибо за конструктивный пример!
                                                          • 0
                                                            Скажите, а куда в этом коде вставить вызов обработчика запроса, когда уже весь запрос из сокета прочитался?

                                                            ///// command processing
                                                            ///

                                                            Это?

                                                            Второй вопрос: если под обработкой подразумевается другой скрипт PHP который с некоторой вероятностью сожрёт память, фатально рухнет или превысит таймаут ответа — как такое вылавливать именно в этой реализации?

                                                            Третий вопрос: nginx при необходимости обработать запрос с помощью интерпретатора php передаёт входной поток в spawn-fcgi или php-fpm через сокеты или tcp, а потом ждёт от них поток ответа, который и возвращает клиенту. На уровне spawn_fgci/php_fpm обработка таких запросов выполняется в отдельных процессах. Количество этих процессов ограничено, а количество запросов на сервер nginx — нет, потому nginx выполняет шедуллинг. Я исходники не читал, могу ошибаться, но общая концепция мне видится именно такой, хотя, шедуллинг может делаться в spawn_fgci/php_fpm. В общем, в вашей реализации как сделать так, что запросов много, а обработчиков мало?
                                                            • 0
                                                              1) Вставить после:
                                                              ////////// А тут мы уже считали ответ и начинаем его парсить

                                                              2) Обрабатывать ответ нужно тоже в неблокирующем режиме :-) Иначе смысла нет.

                                                              3) В нгинксе нет входных потоков и вообще потоков. Есть процессы — воркеры, работающие с буфферами. Каждый воркер грубо имеет описанную в моем коде архитектуру. Текущий код на PHP может держать к примеру 1000 запросов к 10 другим серверам, в точности как делает nginx в режиме обратного проксирования.

                                                            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                                            Самое читаемое