Zend_Http_Client и multi_curl: просто и гибко

Что это?

Очень популярен среди разработчиков PHP Zend Framework. Основное его назначение, конечно, библиотека классов (а не только CMF).
Речь в статье пойдет о компоненте Zend_Http, а точнее его невозможности использовать многопоточные запросы в адаптере cURL.
Образцовый хак zend — решение элегантное, гибкое, с минимумом кода и без внесения изменений в код зенда. Мое решение не идеальное, конечно, но некоторым критериям соответствует.

Хм, а посмотреть?

Итак, схема работы многопоточного клиент http:
Диаграмма работы многопоточного аналога Zend_Http_Client

Примерчик бы...


Как сказал мудрый человек, «Всё познается в сравнении».
  1. // Пример №1. По-старому.
  2. $client = new Zend_Http_Client();
  3. $client->setUri('http://www.google.ru');
  4. $client->setConfig(array('maxredirects' => 0,  'timeout'      => 30));
  5. $response_google = $client->request();
  6.  
  7. $client->setUri('http://ya.ru');
  8. $client->setConfig(array('maxredirects' => 0,  'timeout'      => 30));
  9. $response_ya = $client->request();
  10.  
  11. die($response_google->getBody().$response_habr->getBody());
  12.  
  13. // Пример №2. По-новому.
  14. $client = new App_Http_MultiClient();
  15. $client->setUri('http://www.google.ru');
  16. $client->setConfig(array('maxredirects' => 0,  'timeout'      => 30));
  17. $client->finishRequest();
  18.  
  19. $client->setUri('http://ya.ru');
  20. $client->finishRequest();
  21. $responses = $client->request();
  22.  
  23. die($responses[0]->getBody().$responses[1]->getBody());

Pastebin

И как это работает?


Исходный код файла клиента можно привести полностью:

  1. <?php
  2. /**
  3.  * клиент Используемый для работы с http (MULTI curl);
  4.  * @author mapron
  5.  * @see Zend_Http_Client
  6.  * @see App_Http_ClientAdapter_Multi
  7.  */
  8. class App_Http_MultiClient extends Zend_Http_Client
  9. {
  10.     protected $requests = array();
  11.        
  12.     protected $fieldsCopy = array( 'headers', 'method', 'paramsGet', 'paramsPost', 'enctype', 'raw_post_data', 'auth',
  13.     'files' ); 
  14.        
  15.     /**
  16.      * Данная функция завершает текущий запрос и помещает его в очередь.
  17.      * @param unknown_type $id
  18.      */
  19.     public function finishRequest($id = null)
  20.     {
  21.         $request = array();            
  22.         foreach ($this->fieldsCopy as $field) {
  23.                 $request[$field] = $this->$field;
  24.         }
  25.         foreach (array('uri', 'cookiejar') as $field) {
  26.                 if (!is_null($this->$field)) {
  27.                     $request[$field] = clone $this->$field;
  28.                 }
  29.         }
  30.        
  31.         $config = $this->config;
  32.         unset($config['adapter']); // это, в принципе, не критично, ибо объект мы создаем вручную в request().
  33.         // Сделано для совместимости с возомжными изменениями в Zend.
  34.        
  35.         $request['_config'] = $this->config;
  36.         if (is_null($id)){ // можно не передавать $id, будет просто индексированный массив.
  37.             $this->requests[] = $request;
  38.         } else {
  39.             $this->requests[$id] = $request;
  40.         }
  41.     }
  42.        
  43.     /**
  44.      * Метод, который обрабатывает все ранее сделанные запросы
  45.      * @see Zend_Http_Client::request()
  46.      */
  47.     public function request()
  48.     {
  49.         if (empty($this->requests)) {
  50.             throw new Zend_Http_Client_Exception('Request queue is empty.');
  51.         }
  52.         $this->redirectCounter = 0;
  53.         $response = null;
  54.                
  55.         // Да, выглядит как грязный хак, но так оно и есть. Данный класс просто не поддерживает другие адаптеры.
  56.         $this->adapter = new App_Http_ClientAdapter_Multi();             
  57.                
  58.         $requests = array();
  59.         $this->last_request = array();
  60.        
  61.         foreach ($this->requests as $id => &$requestPure){
  62.             //  Здесь и ниже немного измененная копия первоначального кода.
  63.            
  64.                 // Копируем параметры для запроса.
  65.             foreach ($this->fieldsCopy as $field) $this->$field = $requestPure[$field];
  66.            
  67.             // обрабатываем URI, приводя к стандартному виду.
  68.             $uri = $requestPure['uri'];
  69.             if (! empty($requestPure['paramsGet'])){
  70.                 $query = $uri->getQuery();
  71.                 if (! empty($query)) {
  72.                     $query .= '&';
  73.                 }
  74.                 $query .= http_build_query($requestPure['paramsGet'], null, '&');
  75.                 $requestPure['paramsGet'] = array();
  76.                 $uri->setQuery($query);
  77.             }
  78.                        
  79.             // тело запроса
  80.             $body = $this->_prepareBody();
  81.             if (! isset($this->headers['host'])) {
  82.                 $this->headers['host'] = array('Host', $uri->getHost());                       
  83.             }
  84.             $headers = $this->_prepareHeaders();
  85.                        
  86.             // записываем по кусочкам в массив
  87.             $request = array();
  88.             $request['host'] = $uri->getHost();
  89.             $request['port'] = $uri->getPort();
  90.             $request['scheme'] = ($uri->getScheme() == 'https'? true: false);
  91.             $request['method'] = $this->method;
  92.             $request['uri'] = $uri;
  93.             $request['httpversion'] = $this->config['httpversion'];
  94.             $request['headers'] = $headers;
  95.             $request['body'] = $body;
  96.             $request['_config'] = $requestPure['_config'];
  97.             $requests[$id] = $request;
  98.         }
  99.        
  100.         // запоминаем запросы, вдруг понадобятся в отладке!
  101.         $this->last_request = $requests;
  102.                
  103.         // выполняем мульти-запрос.
  104.         $this->adapter->execute($requests);
  105.                
  106.         // метод read() на самом деле просто извлекает уже сохраненный результат.
  107.         $responses = $this->adapter->read();
  108.                
  109.         if (! $responses) {
  110.             throw new Zend_Http_Client_Exception('Unable to read response, or response is empty');
  111.         }
  112.      
  113.         // преобразуем ответы в удобный Zend_Http_Response для дальнейшей работы.
  114.         foreach ($responses as &$response){                    
  115.             $response = Zend_Http_Response::fromString($response);             
  116.         }
  117.                
  118.         if ($this->config['storeresponse']) {
  119.             $this->last_response = $response;
  120.         }
  121.                
  122.         return $responses;
  123.     }
  124.  
  125. }
Pastebin
И, частично, код адаптера:

  1. <?php
  2. /**
  3.  * Аналог  Zend_Http_Client_Adapter_Curl, только использует curl_multi для параллельных запросов.
  4.  * @author mapron
  5.  * @see Zend_Http_Client_Adapter_Curl
  6.  */
  7. class App_Http_ClientAdapter_Multi extends Zend_Http_Client_Adapter_Curl
  8. {
  9.     protected $_mcurl = null; // handle мультикурла;
  10.     protected $_handles = array();      // массив с handle curl-запросов.
  11.  
  12.         /**
  13.          * Выполнение запросов с помощью curl_multi
  14.          * @param array $requests массив с запросами
  15.          * @throws Zend_Http_Client_Adapter_Exception
  16.          * @throws Zend_Http_Client_Exception
  17.          */
  18.     public function execute(array $requests)
  19.     {
  20.                
  21.         $this->_mcurl = curl_multi_init();
  22.                
  23.         foreach ($requests as $id => $request) {               
  24.             $this->setConfig($request['_config']);     
  25.             // Do the actual connection
  26.             $_curl = curl_init();
  27.             if ($request['port'] != 80) {
  28.                 curl_setopt($_curl, CURLOPT_PORT, intval($request['port']));
  29.             }
  30.             //… исходное создание запроса, пропущено.      
  31.             curl_multi_add_handle($this->_mcurl, $_curl); // добавляем handle к мультикурлу.
  32.             $this->_handles[$id] = $_curl;
  33.         }
  34.                
  35.         $running = null;
  36.         do{
  37.             curl_multi_exec($this->_mcurl, $running);
  38.             // added a usleep for 0.10 seconds to reduce load
  39.             usleep (100000);
  40.         }
  41.         while ($running > 0);
  42.                
  43.         // get the content of the urls (if there is any)
  44.         $this->_response = array();
  45.         $requestsTexts = array();
  46.         foreach ($this->_handles as $id => $ch) {
  47.             // get the content of the handle
  48.             $resp = curl_multi_getcontent($ch);
  49.                        
  50.             // remove the handle from the multi handle
  51.             curl_multi_remove_handle($this->_mcurl, $ch);                      
  52.             //… здесь   обработка ответа, зендовская.
  53.                        
  54.             if (is_resource($ch)) {
  55.                 curl_close($ch);
  56.             }
  57.         }
  58.         curl_multi_close($this->_mcurl);
  59.  
  60.         return $requestsTexts;
  61.     }
  62. }

Pastebin, полностью

Заключение


Пример полностью рабочий и опробован суровой практикой около года.
Данное решение поддерживает все то же, что и поддерживает стандартный Http_Client с адаптером cURL. Но, понятное дело имеет недостатки:
  1. Работа только с cURL;
  2. Отстутствует подержка авто-редиректов.

Других пока не замечал, но лично для меня это не являлось существенным (редиректы по мере надобности руками, а курл и так есть почти везде). Уж на своем-то сервере точно.
Надеюсь оказался кому-то полезен и готов к критике. Продолжений не будет! В посте уже 100% информации.

Ссылки:
1. Документация Zend_Http_Client


2 комментария

avatar
Это мой пост, спасибо что "спасли" его из песочницы хабра.
Даже не знаю, радоваться или нет.
avatar
Зарегистрируйтесь и он будет переведен на ваш аккант.
Только зарегистрированные и авторизованные пользователи могут оставлять комментарии.