Search
Write a publication
Pull to refresh

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
Tags:
Hubs:
You can’t comment this publication because its author is not yet a full member of the community. You will be able to contact the author only after he or she has been invited by someone in the community. Until then, author’s username will be hidden by an alias.