Работа с http через неблокируемые сокеты

    Понадобилось сделать несколько параллельных http запросов на php. Интуиция подсказывала что делается это через неблокируемые сокеты. В интернете в общем то есть несколько готовых классов для работы с ними, но как всегда захотелось легкости и простоты, поэтому был срочно изобретен новый велосипед. Под катом чуть больше 100 строк кода с комментариями и пример использования.

    <?php
    /**
     * AsyncHttp - класс для работы с http через неблокируемые сокеты
     *
     * @author Jeck (http://jeck.ru)
     */
    class AsyncHttp {
        
    private $sockets = array();
        
    private $threads = array();
        
        
    /**
         * Создает сокет и отправляет http запрос
         * @param string $url адрес на который отправляется запрос
         * @param string $method тип запроса, POST или GET
         * @param array $data данные отправляемые при POST запросом
         * @return int $id идентификатор запроса
         * @return false в случае ошибки
         */
        
    private function request($url$method='GET'$data=array()) {
            
    $parts parse_url($url);
            if (!isset(
    $parts['host'])) {
                return 
    false;
            }
            if (!isset(
    $parts['port'])) {
                
    $parts['port'] = 80;
            }
            if (!isset(
    $parts['path'])) {
                
    $parts['path'] = '/';
            }
            if (
    $data && $method == 'POST') {
                
    $data http_build_query($data);
            } else {
                
    $data false;
            }
            
            
    $socket socket_create(AF_INETSOCK_STREAM0);
            
    socket_connect($socket$parts['host'], $parts['port']);
            
    // Если установить флаг до socket_connect соединения не происходит
            
    socket_set_nonblock($socket);
            
            
    socket_write($socket$method." ".$parts['path']." HTTP/1.1\r\n");
            
    socket_write($socket"Host: ".$parts['host']."\r\n");
            
    socket_write($socket"Connection: close\r\n");
            if (
    $data) {
                
    socket_write($socket"Content-Type: application/x-www-form-urlencoded\r\n");
                
    socket_write($socket"Content-length: ".strlen($data)."\r\n");
                
    socket_write($socket"\r\n");
                
    socket_write($socket$data."\r\n");
            }
            
    socket_write($socket"\r\n");
            
            
    $this->sockets[] = $socket;
            return 
    max(array_keys($this->sockets));
        }
        
        
    /**
         * Выполняет GET запрос с помощью метода AsyncHttp::request
         * @see function request
         * @param string $url
         * @return int $id
         */
        
    public function get($url) {
            return 
    $this->request($url'GET');
        }
        
        
    /**
         * Выполняет POST запрос с помощью метода AsyncHttp::request
         * @see function request
         * @param string $url
         * @param array $data
         * @return int $id
         */
        
    public function post($url$data=array()) {
            return 
    $this->request($url'POST'$data);
        }
        
        
    /**
         * Получает данные из сокетов и возвращает массив идентификаторов
         * успешно выполненных запросов в случае успеха
         * @return bool|array
         */
        
    public function iteration() {
            if (
    count($this->sockets) == 0) {
                return 
    false;
            }
            
    $threads = array();
            foreach (
    $this->sockets as $key => $socket) {
                
    $data socket_read($socket0xffff);
                if (
    $data) {
                    
    $threads[] = $key;
                    
    $this->setThread($key$data);
                    unset(
    $this->sockets[$key]);
                    continue;
                }
            }
            
    // На всякий случай
            
    usleep(5);
            return 
    $threads;
        }
        
        
    /**
         * Устанавливает ответ сокета
         * @return void
         */
        
    private function setThread($id$data) {
            
    $this->threads[$id] = $data;
        }
        
        
    /**
         * Возвращает полученные данные из сокета
         * @param int $id идентификатор сокета
         * @param bool $headers=false если true возвращает данные вместе с заголовками
         * @return bool|array
         */
        
    public function getThread($id$headers=false) {
            if (!isset(
    $this->threads[$id])) {
                return 
    false;
            }
            if (
    $headers) {
                return 
    $this->threads[$id];
            } else {
                return 
    substr($this->threads[$id], strpos($this->threads[$id], "\r\n\r\n") + 4);
            }
        }
    }
    ?>


    Пример:
    <?php
    include './asynchttp.php';
    $async = new AsyncHttp;

    $async->get('http://example.com');
    $async->get('http://example.net');
    $async->get('http://example.org');

    while ((
    $threads $async->iteration()) !== false) {
        foreach (
    $threads as $id) {
            echo 
    $async->getThread($id);
        }
    }
    ?>

    P. S. Используется для ускорения обновления показателей в http://i.pr-cy.ru
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 47

      0
      красиво реализовано, взял на заметку, спасибо!
        +3
        if (count($this->sockets) == ) {

        тут закралась опечатка или это новая магия? :)
          0
          Спасибо, сейчас подправлю. Скорее всего при подсветке побилось, конечно же if (count($this->sockets) == 0) {
            0
            Нет все таки этот парсер хабра неправильно обрабатывал тег font, убрал выделение… Мистика…
          +4
          А CURL не подходит для таких задач?
            0
            Ну curl не подходит если надо постоянно держать некоторое число запросов запущенными. То есть пока не выполниться вся группа запросов, нельзя запустить ещё один, ну или приодеться использовать костыли.
              +2
              Вот это пробовали curl_multi_init (http://us3.php.net/manual/en/function.curl-multi-init.php)?
                –2
                Я знаю как работает curl_multi_init. Как на нем реализовать подобный код?

                for ($i=0;$i<5;$i++) {
                    $async->get('http://example.net/cluster.php');
                }

                while (($threads = $async->iteration()) !== false) {
                    foreach ($threads as $id) {
                        $async->get('http://example.net/cluster.php');
                    }
                }

                То есть сначала запускаем 5 потоков, как только завершается один из них запускаем следующий, таким образом все время будет запущенно 5 потоков. Я не видел подобных примеров на curl, там запросы запускаются группами то есть сначала 5 запросов, потом ещё 5 запросов и т. д.
                  +4
                  Вы определённо не правы.
                  Как раз таки это CURL умеет, я как-то раз реализовывал подобную штуку.

                  См. в xpart.ru/resume/sources.zip исходник «Search Engines Multithread Parser\classes\class.multipager.php»
                    +1
                    мультикурл нестабилен и жрет много ресурсов.
                      0
                      Лично я ничего такого не наблюдал. Можете подробнее рассказать?
                        0
                        Ну есть там засада с потреблением памяти. Я так понимаю, интерфейс построен так, что приходится выделить большой блок памяти заранее. Обходится легко. Вот в библиотеке code.google.com/p/multicurl-library/ даже в примере это сделано.
                          0
                          Cсылка на либу MultiCURL вот: code.google.com/p/multicurl-library/downloads/list

                          Вот небольшое описание на русском языке, с примером: www.weblancer.net/users/tvv/portfolio/231798.html

                          Примечание к описанию: wait() вызывается в самом конце, скачивание начинается сразу при первом же addUrl(). Cкачивание c каждого ресурса происходит абсолютно независимо друг от других ресурсов, для каждой скачки можно истанавливать свои параметры (например, разные прокси, разные параметры авторизации, разные протоколы, включая HTTPS, разные таймауты и т.д.). Параллельно скачиванию можно выполнять любые другие действия.

                          Про память — таки да, неконтролируемо жрет-с. Это недостаток этого решения. Как побороть пока не придумал. Хотя для большинства проектов, где данная либа используется, это не критично.
                        0
                        > мультикурл нестабилен и жрет много ресурсов.

                        «нестабилен» — это не так, весьма стабилен (но в некоторых старых версиях бывают утечки памяти в некоторых случаях! так что всегда старайтесь использовать последние версии)

                        «жрет много ресурсов» — да, согласен, что есть то есть. это плата за универсальность и за следование стандартам, и это стоит того. хотя в любом случае, слово «много» — это понятие относительное, и не думаю что подобное решение в лоб, реализованное на PHP (с поддержкой разных протоколов, проксей, кукисов, таймаутов, редиректов и т.д.), будет потреблять меньше ресурсов.
                  0
                  вообще можно, вот костыли начнутся с получением кода ошибки и ее текста bugs.php.net/bug.php?id=48304
                  0
                  Кстати да, чем не CURL?
                  +2
                  С этими сокетами все хорошо до одного момента, пока не наткнетесь на проблему «получать статус сокета»:
                  например, сервер вдруг затупил и не выдает ответа, в буфере ничего нету, но вы циклически его читаете и думаете что все вычитали, хотя надо просто ждать, так как данные будут чуть позже.

                  И что самое обидное, get_socket_status (которая могла бы помочь) с сокетами socket_create() работать не будет (точнее она не скажет eof для неблокируемого сокета)! Ей подавай fsockopen-сокеты только.

                  Так что ваша реализация хороша, пока сервера отвечают быстро.
                    0
                    Мне нужно было достучаться для своего же сервера просто на другие IP, так что в моем случае этого достаточно, но ничто не мешает добавить для каждого сокета время запуска и проверять в цикле его на таймаут.
                      0
                      Согласен.
                      Правда, если все-таки хоть какие-то данные надо вычитывать, то таймаут, ИМХО, не самое правильное решение проблемы будет.
                      +1
                      Просто надо писать с использованием stream_select().
                      +4
                      Xexe :) Когда автор столкнется с нестабильными и периодически неработающими серверами — начет рварь волосы на голове, но надеюсь доведер реалиализацию до ума. Ни в коем случае не хочу обидеть — просто когда-то давно, еще во времена пхп4 стояла задача написания некоторого бота для обработки 200-300к доменов в сутки. Сами понимаете — никто не мог гарантировать работоспособность хотябы 10% из них, поэтому начались нескольконочные танцы с бубнами вокруг таймаутов (которых у автора вообще не наблюдается) и обманыванием пхп — через локальное обращение к самому себе чтобы запустить очередной пхп процесс (т.е. тупо 5-10 процессов обслуживали стабильно с жесткими таймаутами 300-400 соединений единовременно)

                      Автору предлагаю остановится тогда когда получится стабильно забивать свой канал под 99%, с минимальными затыками (3-5 сек в минуту) аааа себе предлагаю порытся в старых исходниках и написать либу под ZF заодно портировав под пхп5, со стрим_селект работать вроде поудобнее будет
                        0
                        ааа и ну конечно в идеале держатся подальше от pctl который много где не стоит и следить за использованием ресурсов. У меня использовался только канал, процессор и память никак напрягались.
                          0
                          я тоже доставлял себя много анальной боли костылями, плюясь на отвратный курл, плохие доки, неведомые глюки с таймаутами и прочую кривоту, потом просто взял эрланг и за вечер набросал отлично контролируемый кравлер со всеми таймаутами и прочим. работает быстрее, надежнее и гибче курла.
                          –2
                          Хороший велосипед — будем ездить! Спасибо!
                            –3
                            пока вы пишете велосипеды, в мире перл уже давно все написано
                            LWP::Parallel
                              +1
                              а заминусовали видимо от зависти, что в мире Perl есть такое централизованная система обмена полезным кодом, в которой хорошим тоном считается снабдить модуль документацией и тестами (которые к слову запускаются в автоматическом режиме на куче разных платформ чтобы уведомить автора о потенциальных проблемах) и также принимать багрепорты.
                                +1
                                Вы не поверите, но в мире php это тоже есть =)
                                LWP — мощный инструмент, знаю не по наслышке, но речь не о нем, а ваш комментарий немного разошелся с темой обсуждения и провоцирует к холиварам. Реакция на лицо
                                  +1
                                  Eсть, но не очень впечатляет, к сожалению (если вы про pear/pecl) :(

                                  Просто выкладывать очередной написанный класс на хабр и говорить вот мол написал как-то так — потом получается что проект состоит из «скопировал с хабра», «скопировал с какого-нибудь еще сайта».

                                  На мой взгляд интерфейс у класса слегка страдает, отсутствует возможность настройки какой-либо.

                                  Почему бы автору не оформить это в законченный продукт — сделать минимальную документацию — выложить на github, google code, на тот же pear.
                                  При этом можно посмотреть есть ли какая-нибудь уже хорошая библиотека для отправки http/ftp запросов и нельзя ли использовать к примеру threads c ней.
                              –2
                              улыбнуло в комментах кода — «На всякий случай»
                              на какой такой всякий случай? (с) =)))
                                –2
                                На случай если попытка чтения из сокета не вызовет задержки и цикл будет работать в холостую, поэтому там стоит небольшая пауза.
                                  0
                                  дело в том, что задержка будет всегда в случае, если сервер медленный или расположен далеко
                                  как написали выше, это может стать проблемой
                                    –1
                                    Именно по этому «На всякий случай» :)
                                +5
                                А вот здесь рахве нет решения Вашей поблемы
                                www.onlineaspect.com/2009/01/26/how-to-use-curl_multi-without-blocking/
                                ?
                                  0
                                  Да хорошее, решение. Скорее всего оно бы мне подошло. Спасибо буду иметь в виду.
                                    0
                                    Мне тоже понравилось, мне как раз в текущем проекте оно пригодилось. Рад что помог.
                                  0
                                  Хорошее решение, проверю на очень медленных соединениях. Спасибо.
                                    0
                                    > $socket = socket_create(AF_INET, SOCK_STREAM, );

                                    вот тут опечатка, скорее всего побилось в парсере, наверное надо дописать SOL_TCP как третий параметр

                                    я бы на Вашем месте изменил бы имена get, ну и post за ним. Лично для меня get означает что-то получить из класса, а не метод запроса для хттп. Возможно это просто дело привычки, но мне кажется более уместными были бы имена getRequest, postRequest или вроде того.
                                      +4
                                      Если бы это было написано на С с BSD-сокетами, я бы сказал что это пример того, как делать не надо.
                                      Может быть на PHP так принято?

                                      // Если установить флаг до socket_connect соединения не происходит

                                      Скорее происходит, но не сразу. А сразу возвращается ошибка, потому что операция потенциально блокирующая.

                                      socket_set_nonblock($socket);
                                      socket_write($socket, $method." ".$parts['path']." HTTP/1.1\r\n");
                                      socket_write($socket, «Host: ».$parts['host']."\r\n");
                                      socket_write($socket, «Connection: close\r\n»);
                                      if ($data) {
                                      socket_write($socket, «Content-Type: application/x-www-form-urlencoded\r\n»);
                                      socket_write($socket, «Content-length: ».strlen($data)."\r\n");
                                      socket_write($socket, "\r\n");
                                      socket_write($socket, $data."\r\n");
                                      }
                                      socket_write($socket, "\r\n");

                                      Ни одной проверки на то, что socket_write записал все, что его просили записать. Он и в блокирующем-то режиме судя по документации может записать не всё, а в неблокирующем тем более.

                                      $data = socket_read($socket, 0xffff);
                                      if ($data) {
                                      $threads[] = $key;
                                      $this->setThread($key, $data);
                                      unset($this->sockets[$key]);
                                      continue;

                                      Опять же, никакой проверки длины. Наверно это будет работать для мелких ответов. Но по чистой случайности.

                                      // На всякий случай
                                      usleep(5);

                                      Воспользуйтесь socket_select, чтобы отсеять большую часть «всяких» случаев.
                                        0
                                        Не гуглить — здоровью вредить.
                                        code.google.com/p/multicurl-library/
                                          0
                                          не читать предыдущие комментарии тоже плохо ;)
                                            0
                                            этот.парент комментарий к комментарию pwlnw выше
                                              +1
                                              Тот велосипед значительно практичней чистых функций: там есть очередь и регулируется число одновременных соединений.
                                          • НЛО прилетело и опубликовало эту надпись здесь
                                            • НЛО прилетело и опубликовало эту надпись здесь
                                                0
                                                тогда будет уже две проблемы: написать обертку вокруг libev и написать программу на php, которая будет использовать эту обертку. :)
                                              • НЛО прилетело и опубликовало эту надпись здесь
                                                  0
                                                  2009 =). По вашей статье разберусь с сокетами.

                                                  Никогда не поздно — Спасибо!

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

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