19 Сентябрь 2007...19:32

Многопоточность в PHP: HTTP-клиент

Перейти к комментариям

Давненько ничего полезного не писал, посему спешу исправиться.

Собственно, эта тема обсасывалась уже уже не раз и не два (призываю Google в свидетели), поэтому не буду разводить тряхомудиюдемагогию, а сразу приведу исходник класса, который позволяет довольно просто выполнять несколько HTTP-запросов параллельно:

class HttpQueue
{
    /**
     * An array of URLs
     *
     * @access  private
     * @var     array
     */
    private $_urls = array(); 

    /**
     * An array of server sockets
     *
     * @access  private
     * @var     array
     */
    private $_sockets = array(); 

    /**
     * An array of server responses
     *
     * @access  private
     * @var     array
     */
    private $_response = array(); 

    /**
     * Socket timeout
     *
     * @access  private
     * @var     integer
     */
    private $_timeout = 30; 

    /**
     * An array of sockets which can be received
     *
     * @access  private
     * @var     array
     */
    private $_read = array(); 

    /**
     * An array of sockets which can be sended
     *
     * @access  private
     * @var     array
     */
    private $_write = array(); 

    /**
     * Adds an URL into a tasklist
     *
     * @access  public
     * @param   string  $method
     * @param   string  $url
     * @return  void
     */
    public function add($method, $url)
    {
        $this->_urls[] = array(strtoupper($method), $this->_parseUrl($url));
    } 

    /**
     * Parses requested URL and checks for all URL parts
     *
     * @access  private
     * @param   string   $url
     * @return  array
     */
    private function _parseUrl($url)
    {
        $parts = parse_url($url);
        $parts['port'] = array_key_exists('port', $parts) ? $parts['port'] : 80;
        $parts['sock'] = sprintf('%s:%s', $parts['host'], $parts['port']);
        $parts['request'] = sprintf('%s?%s', $parts['path'], $parts['query']);
        return $parts;
    } 

    /**
     * Starts fetch process
     *
     * @access  public
     * @param   void
     * @return  array
     */
    public function fetch()
    {
        $this->_create();
        $this->_process();
        return $this->toArray();
    } 

    /**
     * Sets socket timeout (in seconds)
     *
     * @access  public
     * @param   integer  $timeout
     * @return  void
     */
    public function setTimeout($timeout)
    {
        $this->_timeout = $timeout;
    } 

    /**
     * Returns array of server responses
     *
     * @access  public
     * @param   void
     * @return  array
     */
    public function toArray()
    {
        return $this->_response;
    } 

    /**
     * Creates socket conenctions with hosts given in URL
     *
     * @access  private
     * @param   void
     * @return  void
     */
    private function _create()
    {
        foreach ($this->_urls as $id => $connect) {
            if ($socket = @stream_socket_client($connect[1]['sock'], $errno,
                $errstr, $this->_timeout,
                STREAM_CLIENT_ASYNC_CONNECT|STREAM_CLIENT_CONNECT)) {
                $this->_sockets[$id] = $socket;
                $this->_response[$id]  = 'processing';
                continue;
            } 

            $this->_response[$id] = "failed, $errno $errstr";
        }
    } 

    /**
     * Process opened sockets
     *
     * @access  private
     * @param   void
     * @return  void
     */
    private function _process()
    {
        while (count($this->_sockets)) {
            $this->_read  = $this->_sockets;
            $this->_write = $this->_sockets; 

            $select = stream_select($this->_read, $this->_write, $e = null,
                                    $this->_timeout); 

            if ($select > 0) {
                $this->_read();
                $this->_write();
            } 

            else {
                foreach ($this->_sockets as $id => $s) {
                    $this->_response[$id] = 'Timed out';
                } 

                break;
            }
        }
    } 

    /**
     * Receives data from readable socket
     *
     * @access  private
     * @param   void
     * @return  void
     */
    private function _read()
    {
        foreach ($this->_read as $fp) {
            $id = array_search($fp, $this->_sockets);
            $data = fread($fp, 8192); 

            if (strlen($data) == 0) {
                if ($this->_response[$id] == 'processing') {
                    $this->_response[$id] = 'failed to connect';
                } 

                fclose($fp);
                unset($this->_sockets[$id]);
            } 

            else {
                $this->_response[$id].= $data;
            }
        }
    } 

    /**
     * Sends HTTP request into writeable socket
     *
     * @access  private
     * @param   void
     * @return  void
     */
    private function _write()
    {
        foreach ($this->_write as $fp) {
            $id = array_search($fp, $this->_sockets);
            $method  = $this->_urls[$id][0];
            $request = $this->_urls[$id][1]['request'];
            $host    = $this->_urls[$id][1]['host']; 

            fputs($fp, "$method $request HTTP/1.0rn");
            fputs($fp, "Host: $hostrnrn"); 

            $this->_response[$id] = '';
        }
    }
}

Ну и, разумеется, пример использования:

$http = new HttpQueue();
$http->setTimeout(20);
$http->add('get', 'http://ya.ru');
$http->add('get', 'http://google.com');
$http->add('get', 'http://wordpress.com');
$result = $http->fetch();

Чтобы добавить URL в очередь, используется метод HttpQueue::add($method, $url). Первый параметр указывает тип метода передачи данных (в данном классе заработают GET и HEAD; чтобы заработал POST, нужно внести несколько изменений, но это никому не интенесно :) ), второй — собственно URL.

В результирующем массиве $result будут храниться ответы серверов в том порядке, в котором они были заданы.

P.S. Если я буду продолжать публиковаться, то со временем перестану напоминать, что мои классы работают на PHP4.x и младше :)

P.P.S. Прошу сильно не пинать ногами за комментарии в DocBlock: по-английски пишу как умею.

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

  • 1. Класс и объектно-ориентированность не одно и тоже и писать класс вряд ли надо было.
    2. Пулл неблокирующих сокетов и ряд других идей действительно описаны не раз, но это не МНОГОПОТОЧНОСТЬ, увы. В Perl она есть: threads&fork – вставляй куда хочешь, за исключением работы с файлами, а это лишь сетевые запросы один за другим.

  • Спасибо за комментарий прежде всего :) . А теперь по пунктам:

    Класс и объектно-ориентированность не одно и тоже и писать класс вряд ли надо было.

    Насчет того, что класс и объектная ориентированность не одно и то же, согласен на сто процентов. А вот насчет “вряд ли надо было”… Разрешите узнать, а почему, собсна?.. :) И как бы Вы реализовали эту задачу? :)

    Пулл неблокирующих сокетов и ряд других идей действительно описаны не раз, но это не МНОГОПОТОЧНОСТЬ, увы. В Perl она есть: threads&fork – вставляй куда хочешь, за исключением работы с файлами, а это лишь сетевые запросы один за другим

    Тут дело вот в чем… Думаю, что не стоит погрязать в определениях; достаточно знать, что при использовании этого класса запрос #2 начнет выполняться до того, как обработка запроса #1 полностью завершится. На мой взгляд, это многопоточность :) . А вот fork, кстати, нет: это распараллеливание процессов, а не многопоточность. Не годится.

    Между прочим, в теле статьи я ни разу не употребил слово «многопоточность». Там написано примерно так:

    позволяет довольно просто выполнять несколько HTTP-запросов параллельно

    Что касается заголовка… Каюсь, слово многопоточность в заголовке поста я употребил специально, чтобы собирать трафик по этому запросу, который имеет некоторую популярность. Прошу меня простить за эту маленькую ложь :) . Что ж поделать, законы рынка: не обманешь — не продашь :)

  • fork – не многопоточность? оригинально. Наверное, я что-то забыл о кодинге под unix. =( Если говорить о fork’е, то есть важная вещь – многопоточность непродуктивна при скажем переборе словаря, потоков эдак >50M. Треды дохнут, а если нет, то виснут при моменте соединения. Тот же fork на микрозадачах – открыть файл#1, потом файл#2.. файл#т<100. – слишком грузен и выигрыш от многопоточности стремится к нулю. Треды – это не совсем потоки смысла “потоков”. Это потоки, но сильно урезанные, они для микрозадач. А fork создаёт, конечно, полноправные процессы с дочерним PID и создан был для задач макроуровня.
    Но многопоточности на php пока нет. Многие пытаются доказать и в других средствах СМИ, что вот мы можем отправить несколько запросов и получить ответы, ряд приложений построен на этом принципе, но многопоточность есть в php лишь для сети. Для локального воздействия с файлам/процессами/обработки текстовой информации – у него нет. Он может лишь послать несколько запросов и получить ответы без создания очереди.
    А так многопоточности нет.

  • К вопросу о объектно-ориентированности: как бы я реализовал. Максимум – функция. Мне, честно, не совсем интересен Java или, скажем, C#, ибо – классизированны и ОО до конца. Мне мало представляет интерес код:

    class Hello_world
    {
    /**
    * Text variable for print
    *
    * @access private
    * @var string
    */
    private $text = "Hello, world!";
    /**
    * Function for print text
    *
    * @access public
    * @param string $var
    * @return void
    */
    public function print_text($var)
    {
    print $var;
    }
    }

    А к классу ещё пара интерфейсов и абстрактный класс.
    Для меня важна рациональность использования памяти. Класс заполняет несколько связок p-блоков в ОО движке php.

    - чтобы собирать трафик по этому запросу

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

    php(2),
    web2.0(238),
    firefox(41),
    opera(39),
    ie7(12),
    многопоточность(713),
    как заработать денех в сети(1940)

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

    А fork всё ранво не многопоточность, а распараллеливание процессов.

    Про стиль программирования: разумеется, писать класс для вывода ‘Hello World’ не всегда нужно (Ваш класс, кстати, вообще непонятно что делает). Но это уже больше вопрос проектирования. Если вывод текста является бизнес-частью приложения, а не простым дизайном, то почему бы и нет…

    Про теги: я буду писать про то, что мне интересно :) . Про PHP интересно, про Web2.0 — не особо. Про Оперу, FF и многопоточность всё написали до меня, IE7 терпеть ненавижу, а о том, как заработать денех в сети, буду молчать как рыба об лёд: кому надо, тот в теме :)

  • Не подскажите, куда можно дописать user-agent и header?

  • User-Agent можно дописать туда же, где расположены другие заголовки (Host:, к примеру): смотрите тело метода _write(), на строки с fputs() :)

    Кстати, Вордпресс изгадил код, бекслеши повырезал. Разумеется, там не “rn” должно быть, а “\r\n”…


Ответить

You must be logged in to post a comment.

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