Zend_Http_Client и multi_curl: просто и гибко
Что это?
Очень популярен среди разработчиков PHP Zend Framework. Основное его назначение, конечно, библиотека классов (а не только CMF).Речь в статье пойдет о компоненте Zend_Http, а точнее его невозможности использовать многопоточные запросы в адаптере cURL.
Образцовый хак zend — решение элегантное, гибкое, с минимумом кода и без внесения изменений в код зенда. Мое решение не идеальное, конечно, но некоторым критериям соответствует.
Хм, а посмотреть?
Итак, схема работы многопоточного клиент http:Примерчик бы...
Как сказал мудрый человек, «Всё познается в сравнении».
- // Пример №1. По-старому.
- $client = new Zend_Http_Client();
- $client->setUri('http://www.google.ru');
- $client->setConfig(array('maxredirects' => 0, 'timeout' => 30));
- $response_google = $client->request();
- $client->setUri('http://ya.ru');
- $client->setConfig(array('maxredirects' => 0, 'timeout' => 30));
- $response_ya = $client->request();
- die($response_google->getBody().$response_habr->getBody());
- // Пример №2. По-новому.
- $client = new App_Http_MultiClient();
- $client->setUri('http://www.google.ru');
- $client->setConfig(array('maxredirects' => 0, 'timeout' => 30));
- $client->finishRequest();
- $client->setUri('http://ya.ru');
- $client->finishRequest();
- $responses = $client->request();
- die($responses[0]->getBody().$responses[1]->getBody());
Pastebin
И как это работает?
Исходный код файла клиента можно привести полностью:
- <?php
- /**
- * клиент Используемый для работы с http (MULTI curl);
- * @author mapron
- * @see Zend_Http_Client
- * @see App_Http_ClientAdapter_Multi
- */
- class App_Http_MultiClient extends Zend_Http_Client
- {
- protected $requests = array();
- protected $fieldsCopy = array( 'headers', 'method', 'paramsGet', 'paramsPost', 'enctype', 'raw_post_data', 'auth',
- 'files' );
- /**
- * Данная функция завершает текущий запрос и помещает его в очередь.
- * @param unknown_type $id
- */
- public function finishRequest($id = null)
- {
- $request = array();
- foreach ($this->fieldsCopy as $field) {
- $request[$field] = $this->$field;
- }
- foreach (array('uri', 'cookiejar') as $field) {
- if (!is_null($this->$field)) {
- $request[$field] = clone $this->$field;
- }
- }
- $config = $this->config;
- unset($config['adapter']); // это, в принципе, не критично, ибо объект мы создаем вручную в request().
- // Сделано для совместимости с возомжными изменениями в Zend.
- $request['_config'] = $this->config;
- if (is_null($id)){ // можно не передавать $id, будет просто индексированный массив.
- $this->requests[] = $request;
- } else {
- $this->requests[$id] = $request;
- }
- }
- /**
- * Метод, который обрабатывает все ранее сделанные запросы
- * @see Zend_Http_Client::request()
- */
- public function request()
- {
- if (empty($this->requests)) {
- throw new Zend_Http_Client_Exception('Request queue is empty.');
- }
- $this->redirectCounter = 0;
- $response = null;
- // Да, выглядит как грязный хак, но так оно и есть. Данный класс просто не поддерживает другие адаптеры.
- $this->adapter = new App_Http_ClientAdapter_Multi();
- $requests = array();
- $this->last_request = array();
- foreach ($this->requests as $id => &$requestPure){
- // Здесь и ниже немного измененная копия первоначального кода.
- // Копируем параметры для запроса.
- foreach ($this->fieldsCopy as $field) $this->$field = $requestPure[$field];
- // обрабатываем URI, приводя к стандартному виду.
- $uri = $requestPure['uri'];
- if (! empty($requestPure['paramsGet'])){
- $query = $uri->getQuery();
- if (! empty($query)) {
- $query .= '&';
- }
- $query .= http_build_query($requestPure['paramsGet'], null, '&');
- $requestPure['paramsGet'] = array();
- $uri->setQuery($query);
- }
- // тело запроса
- $body = $this->_prepareBody();
- if (! isset($this->headers['host'])) {
- $this->headers['host'] = array('Host', $uri->getHost());
- }
- $headers = $this->_prepareHeaders();
- // записываем по кусочкам в массив
- $request = array();
- $request['host'] = $uri->getHost();
- $request['port'] = $uri->getPort();
- $request['scheme'] = ($uri->getScheme() == 'https'? true: false);
- $request['method'] = $this->method;
- $request['uri'] = $uri;
- $request['httpversion'] = $this->config['httpversion'];
- $request['headers'] = $headers;
- $request['body'] = $body;
- $request['_config'] = $requestPure['_config'];
- $requests[$id] = $request;
- }
- // запоминаем запросы, вдруг понадобятся в отладке!
- $this->last_request = $requests;
- // выполняем мульти-запрос.
- $this->adapter->execute($requests);
- // метод read() на самом деле просто извлекает уже сохраненный результат.
- $responses = $this->adapter->read();
- if (! $responses) {
- throw new Zend_Http_Client_Exception('Unable to read response, or response is empty');
- }
- // преобразуем ответы в удобный Zend_Http_Response для дальнейшей работы.
- foreach ($responses as &$response){
- $response = Zend_Http_Response::fromString($response);
- }
- if ($this->config['storeresponse']) {
- $this->last_response = $response;
- }
- return $responses;
- }
- }
И, частично, код адаптера:
- <?php
- /**
- * Аналог Zend_Http_Client_Adapter_Curl, только использует curl_multi для параллельных запросов.
- * @author mapron
- * @see Zend_Http_Client_Adapter_Curl
- */
- class App_Http_ClientAdapter_Multi extends Zend_Http_Client_Adapter_Curl
- {
- protected $_mcurl = null; // handle мультикурла;
- protected $_handles = array(); // массив с handle curl-запросов.
- /**
- * Выполнение запросов с помощью curl_multi
- * @param array $requests массив с запросами
- * @throws Zend_Http_Client_Adapter_Exception
- * @throws Zend_Http_Client_Exception
- */
- public function execute(array $requests)
- {
- $this->_mcurl = curl_multi_init();
- foreach ($requests as $id => $request) {
- $this->setConfig($request['_config']);
- // Do the actual connection
- $_curl = curl_init();
- if ($request['port'] != 80) {
- curl_setopt($_curl, CURLOPT_PORT, intval($request['port']));
- }
- //… исходное создание запроса, пропущено.
- curl_multi_add_handle($this->_mcurl, $_curl); // добавляем handle к мультикурлу.
- $this->_handles[$id] = $_curl;
- }
- $running = null;
- do{
- curl_multi_exec($this->_mcurl, $running);
- // added a usleep for 0.10 seconds to reduce load
- usleep (100000);
- }
- while ($running > 0);
- // get the content of the urls (if there is any)
- $this->_response = array();
- $requestsTexts = array();
- foreach ($this->_handles as $id => $ch) {
- // get the content of the handle
- $resp = curl_multi_getcontent($ch);
- // remove the handle from the multi handle
- curl_multi_remove_handle($this->_mcurl, $ch);
- //… здесь обработка ответа, зендовская.
- if (is_resource($ch)) {
- curl_close($ch);
- }
- }
- curl_multi_close($this->_mcurl);
- return $requestsTexts;
- }
- }
Pastebin, полностью
Заключение
Пример полностью рабочий и опробован суровой практикой около года.
Данное решение поддерживает все то же, что и поддерживает стандартный Http_Client с адаптером cURL. Но, понятное дело имеет недостатки:
- Работа только с cURL;
- Отстутствует подержка авто-редиректов.
Других пока не замечал, но лично для меня это не являлось существенным (редиректы по мере надобности руками, а курл и так есть почти везде). Уж на своем-то сервере точно.
Надеюсь оказался кому-то полезен и готов к критике. Продолжений не будет! В посте уже 100% информации.
Ссылки:
1. Документация Zend_Http_Client
2 комментария
Даже не знаю, радоваться или нет.