"Правильный" multi_curl в PHP

версия для печати

Мне нужно было разобраться, как именно работают функции curl_multi_*, представленные в PHP, почему так разнятся примеры не только в инете, но даже в справке PHP, что на самом деле происходит. CURL в PHP - это инкапсуляция настоящих функций библиотеки и еще некоторых других. При этом PHP использует не все функции libcurl, да и справка оставляет желать лучшего. Дабы не тыкать вслепую пимпы на клаве, перебирая варианты чужого кода, я решил постичь истинный смысл функций мульти-cURL и их параметров.

Статья получилась длинная, поэтому Содержание:

API библиотеки libcurl на С

По большей части здесь представлен перевод страниц сайта curl.haxx.se с моими соображениями в дополнение. Многие из приведенных здесь функций окажутся неизвестными для PHP-кодера. Cвязь двух языков рассмотрена ниже.

Основная часть

Пару слов о терминологии (без претензии на абсолютную правильность):

  • если вам неудобно слово "трансфер", можете заменить на "передача". Как угодно.
  • простой дескриптор ("easy handle") - это то, что создается функцией curl_init(), т.е. дескриптор объекта cURL.
  • аббревиатура "cURL" и название "libcurl" в этой статье означают одно и тоже.
  • здесь речь пойдет в основном о мульти-интерфейсе библиотеки libcurl, если не указано иное. Т.е. статья про набор функций cURL для организации множества одновременных запросов к web-серверу. Что мне непонятно: говорим об интерфейсе, т.е. заготовках, "контракте" на наличие функций и их точное описание. А где собственно реализация этих функций, где конкретный код? Нужно учесть, что речь о использовании cURL на C, где, строго говоря, вообще нет интерфейсов. Может в таком случае можно приравнять понятие интерфейса к коду функций? Или я просто путаю понятия :)

Простой интерфейс libcurl представляет собой синхронный интерфейс, который передает один файл за раз и не возвращает результат, пока не закончит. Мульти-интерфейс позволяет вашей программе передавать много файлов в обоих направлениях одновременно, не принуждая вас использовать много потоков. Из названия может показаться, что мульти-интерфейс предназначен для многопоточных приложений (игра слов английского языка - "multi interface" и "multi-threaded"), но на деле все почти наоборот. Мульти-интерфейс позволяет однопоточному приложению выполнять такие же виды множественных, одновременных трансферов, какие может выполнять многопоточная программа. Он позволяет получить выгоды многопоточных трансферов без сложностей управления и синхронизации многих потоков.

Чтобы использовать этот интерфейс, вам лучше сначала понять, как пользоваться простым интерфейсом libcurl (здесь не рассказываю). Мульти-интерфейс это простой способ выполнить множество трасферов одновременно путем добавления простых дескрипторов в "мульти стек" (далее - "стек").

Примерный порядок работы такой: создать простые дескрипторы, описать все нужные для них опции (для каждого дескриптора могут быть свои опции). Далее создать мульти-дескриптор функцией curl_multi_init(), затем добавить в него простые дескрипторы вызовом curl_multi_add_handle(). Замечание: во избежание глюков простой дескриптор нельзя использовать, пока он находится в стеке. Когда добавлены все дескрипторы на текущий момент (можно добавить еще в любое время), запустить трансферы вызовом curl_multi_perform().

CURLMcode curl_multi_perform (CURLM *multi_handle, int *running_handles)

"perform" (англ). - выполнять. Функции curl_multi_init и curl_multi_add_handle нормально описаны в справке PHP. "Аналогом" curl_multi_perform() является curl_multi_exec.

Тип CURLMcode - это тот же integer, только значения соответствуют кодам ошибок мульти-cURL. Коды ошибок cURL вместе с пояснениями (на английском).

Функция curl_multi_perform() является ассинхронной. Она выполняется настолько мало, насколько это возможно, и возвращает контроль ваше программе. Она разработана так, чтобы никогда не блокировать (полагаю, речь о блокировании программы - прим. пер.).

Одна из основных идей мульти-интерфейса - позволить вашему приложению управлять ситуацией. Вы управляете трансферами вызывая curl_multi_perform(). После этого libcurl начнет передачу данных, если будет что передавать. Она будет использовать callback-функции и все, что вы указали в настойках простых дескрипторов (см. функцию curl_easy_setopt() или PHP-аналог curl_setopt()). Она будет передавать данные по всем тем трансферам в стеке, которые будут готовы передавать что-либо. Это может быть весь стек, а может ни одного трансфера.

До версии 7.20.0: если в функция возвращает CURLM_CALL_MULTI_PERFORM, обычно это означает, что вам нужно вызвать curl_multi_perform() снова, до select() или других действий (об этом чуть позже). Не обязательно делать это немедленно, но этот код возврата означает, что у libcurl возможно есть еще данные для возврата или отправки перед тем, как она будет "удовлетворена".

На заметку, функция curl_multi_perform() возращает CURLM_CALL_MULTI_PERFORM только когда хочет немедленно быть вызванной снова. Если все в порядке и нет ничего срочного, то функция возвращает CURLM_OK и вам нужно ждать активности, чтобы вызвать эту функцию снова.

Как я не извращался, не смог получить другие CURLM_* коды, кроме CURLM_CALL_MULTI_PERFORM и CURLM_OK. Устойчивая система :)

В своем втором параметре эта функция хранит количество еще работающих трансферов (а не флаг, как сказано в PHP-справке), т.е. тех, кто еще не закончил прием/передачу данных. Трансферы могут завершаться ошибкой передачи, это тоже вариант. Когда какой-то трансфер завершится, счетчик уменьшится. Когда он станет равен 0, значит все транферы завершились.

Любопытный момент, который я обнаружил экспериментально: похоже, curl_multi_add_handle() кидает в стек копию простого дескриптора. К примеру, если закрыть дескриптор сразу после добавления, multi_curl_perform() все равно его запустит и обработает. Даже передача по ссылке, типа curl_multi_add_handle($mh, &$ch2), не меняет этот факт.

Функцию curl_multi_info_read() можно использовать для получения информации по завершенным трансферам.

CURLMsg *curl_multi_info_read ( CURLM *multi_handle, int *msgs_in_queue)

Второй параметр указывает на число непросмотренных сообщений о завершенных трасферах. Т.о. если вызывать эту функцию в цикле, пока не закончатся сообщения (будет NULL), можно получить инфу по всем завершенным трансферам на данный момент.

Если нужно остановить передачу по одному простому дескриптору в стеке, используйте curl_multi_remove_handle. После curl_multi_remove_handle() все еще можно использовать простой дескриптор для одиночных функций cURL, типа curl_exec() и т.п. Поэтому исключения дескриптора из стека не достаточно для корректного завершения работы. Нужно вызывать еще curl_easy_cleanup() для каждого дескриптора. После всей работы нужно закрыть мульти-дескриптор вызовом curl_multi_cleanup(). PHP-аналоги: curl_close и curl_multi_close.

Высший пилотаж

Сказанного выше уже достаточно, чтобы осознано использовать функции curl_multi_* в PHP. Однако, чтобы скрипт по минимуму использовал ресурсы сервера, а код выглядел более профессионально, нужно знать еще пару вещей.


Небольшой экскурс в язык программирования C. int select (int nfds, fd_set *restrict readfds, fd_set *restrict writefds, fd_set *restrict errorfds, struct timeval *restrict timeout);

Функция select() (или pselect()) - это ключевая функция для программ на C, обрабатывающих несколько файловых дескрипторов (или сокетов) одновременно. Массивы файловых дескрипторов называются наборами дескрипторов. Каждый набор объявлен как fd_set (структурный тип). Обычно select() используется для ожидания изменения статуса у одного или нескольких файловых дескрипторов. Изменение статуса происходит, когда в дескрипторе появляются данные для чтения, или когда во внутренних буферах ядра появляется место и в файловый дескриптор может быть произведена запись, или когда в дескрипторе происходит ошибка (в случае сокета или канала (pipe) такая ошибка возникает при закрытии другого участника соединения). Проще говоря, функция select() мониторит множество дескрипторов одновременно и своевременно погружает процесс в состояние сна при отсутствии активности. (по материалам сайта amax.h16.ru))

Если ни один из заданных дескрипторов не готов к назначенным действиям, то select() блокирует скрипт, пока хотя бы один дескриптор не будет готов, или пока не наступит таймаут, или выполнение не прервется сигналом. Параметр timeout определяет, как долго функция select() должна ждать. Если время вышло, функция должна завершиться.

int poll (struct pollfd fds[], nfds_t nfds, int timeout);

Функция poll() определит файловые дескрипторы готовые для чтения/записи данных, или имеющие другие события. В последнем параметре функции устанавливается таймаут ожидания в миллисекундах. Если проверка сразу не выявила активности на выбранных файловых дескрипторах, poll() должна ждать активности на дескриторах, как минимум, указанный период времени. Если таймаут установлен 0, то poll() должна сразу закончить свою работу и вернуть значение; если -1, тогда приложение блокируется, пока не наступит требуемое событие или вызов функции не будет прерван.

Это частичный перевод man-ов раздела "3 - библиотечные функции" См. так же в разделе "2 - системные вызовы" select(2), poll(2).


Теперь самое интересное. Ваше приложение может узнать у libcurl, когда библиотека готова к вызову для передачи данных. Поэтому не нужно дергать в цикле curl_multi_perform() до безумия. Функция curl_multi_fdset() предлагает интерфейс, которым вы можете извлекать наборы дескрипторов типа fd_set из libcurl, чтобы использовать вызовы select() или poll() для получения информации, когда трансферам в стеке возможно нужно внимание.

CURLMcode curl_multi_fdset (CURLM *multi_handle, fd_set *read_fd_set, fd_set *write_fd_set, fd_set *exc_fd_set, int *max_fd)

Очень важный момент здесь. Функция curl_multi_fdset() извлекает информацию о файловых дескрипторах из переданного ей мульти-дескриптора. Ее вызов заполнит набор fd_set конкретными файловыми дескриторами, используемыми интрефейсом libcurl в данный момент. Если в libcurl не задано ни одного файлового дескриптора, параметр функции max_fd будет содержать -1. Это значит, что libcurl делает что-то, что нельзя мониторить приложением через сокет. В этом случае вы не можете узнать, когда точно закончится текущее действие с использованием select(). Поэтому когда max_fd=-1, вам нужно немного подождать и затем вызвать curl_multi_perform() в любом случае. Сколько ждать? Я бы предложил 100 мс, как минимум, но вы можете сами найти наиболее подходящее значение.

Как выглядит на практике описанное выше? Если libcurl в момент запроса по select() с установленным таймаутом вернет max_fd=-1, то программа зависнет на весь таймаут, не зависимо от дальнейшей активности файловых дескрипторов. Потом все заработает корректно. Т.е. указал таймаут 10 секунд, висим 10 секунд, потом работаем без задержек. Этот ньюанс может проявиться, если web-сервер стоит на медленной машине или при сильной загрузке нормального сервера. К примеру, у меня на локальной машине был косяк с зависанием, а на сервере хостера - нет.

Большинство приложений использует curl_multi_fdset() для получения файловых дескрипторов из мульти-дескриптора libcurl. Затем приложение ждет каких-либо действий используя select(), и когда хотя бы один дескриптор готов, вызывает curl_multi_perform().

Итак, вызываем curl_multi_fdset(), затем select(). Она вернет значение, когда один из файловых дескрипторов даст сигнал действия, и тогда вызываем curl_multi_perform(), чтобы позволить libcurl сделать то, что она хочет сделать. В этом суть контроля над ситуацией. А теперь ложка дегтя в эту кашу: примите во внимание, что у libcurl имеется своя возможность следить за таймаутом реакции, поэтому не следует назначать слишком длинное ожидание в select(), и нужно вызывать время от времени curl_multi_perform(), даже если не было сигналов от файловых дескрипторов (дословный перевод). Вот это миленько: какое точное понятие в программировании - "не слишком долго, время от времени". Замечу, что по умолчанию таймаут select() - 1сек, таймаут libcurl на ожидание реакции на сокетах ~5 мин (на форумах вычитал). Еще одно предостережение: обязательно вызвайте curl_multi_fdset() сразу перед вызовом select(), т.к. текущий набор файловых дескрипторов мог измениться с прошлого вызова функций libcurl.

Лирическое отступление. Вопрос с правильным таймаутом оказался практически не разрешимым :( На том же сайте нашел следующее: "когда выполняете select(), вам следует использовать curl_multi_timeout() для выяснения нужного времени ожидания действия. Вызовите curl_multi_perform() даже если нет активности на указанных файловых дескрипторах по истечении таймаута". Так вот, функция curl_multi_timeout() выясняет значение либо через вызов curl_multi_socket_action() задав второму параметру значение CURL_SOCKET_TIMEOUT, либо через вызов той же curl_multi_perform()! В пером случае без бутылки не разобраться, но все-таки можно докопаться до истины. Но каким образом вызов curl_multi_perform установит таймаут для select, я не понял :( Более того, покопавшись в исходниках PHP, я выяснил, что там таймаут расчитывается математикой от переданного параметра $timeout (double), затем задается в select(). Т.е. ни каких дополнительных вызовов curl_multi_* в PHP не делается.

Вся прелесть в том, что описанные заморочки с curl_multi_fdset(), select() и иже с ними инкапсулированы в одну функцию PHP - curl_multi_select().

Реализация на PHP

Рассматриваем PHP 5.3.8, cURL 7.20.0. Ниже приведен список некоторых функций cURL в PHP и их связь с "настоящими" функциями библиотеки. В исходниках PHP помимо вызова "настоящих" функций libcurl есть еще обращения к zend_* и много того, что мне непостижимо. Но главную суть я уловил и подтвердил свои догадки. Что обо всем этом написано в справке PHP, можно узнать здесь.

cURL на PHPИнкапсуляция (код С)
int curl_multi_exec (resource $mh,
int &$still_running)
curl_multi_perform (mh->multi, &still_running)
RETURN_LONG
В текущей версии PHP не используется curl_multi_socket()
int curl_multi_select ( resource $mh
[, float $timeout = 1.0 ] )
_make_timeval_struct (&to, timeout); расчет таймаута (внутренняя функция)
curl_multi_fdset (mh->multi, &readfds, &writefds, &exceptfds, &maxfd);
select (maxfd+1, &readfds, &writefds, &exceptfds, &to);
RETURN_LONG
void curl_multi_close (resource $mh)curl_multi_cleanup (mh->multi);
bool curl_setopt (resource $ch,
int $option, mixed $value)
curl_easy_setopt (ch->cp, option, value);
return 1;
void curl_close (resource $ch)curl_easy_cleanup (ch->cp);
Практика

Теперь зная, откуда "ноги растут", можно создать правильный код на PHP. Приведенный мной пример не претендует на единственно возможный, это один из многих рабочих вариантов использования мульти-cURL. Весь код здесь не привожу, см. в этом архиве. Код в архиве немного другой, больше служебной информации выдает в браузер.

$urls  =  array(
   'http://localhost/test.php',       'http://localhost/test.php?p = 2',
   'http://localhost/test.php?p = 2', 'http://localhost/test.php?p = 5',
   'http://localhost/test1.php',      'http://localhost/test.php?p = 10',
);

$mh = curl_multi_init(); //создаем набор дескрипторов cURL

foreach ($urls as $i=>$v){
    $ch[$i] = curl_init($v);
    curl_setopt($ch[$i], CURLOPT_HEADER, 0);          //Не включать заголовки в ответ
    curl_setopt($ch[$i], CURLOPT_RETURNTRANSFER, 1);  //Убираем вывод данных в браузер
    curl_setopt($ch[$i], CURLOPT_CONNECTTIMEOUT, 30); //Таймаут соединения
    curl_multi_add_handle($mh, $ch[$i]);
}

while (curl_multi_exec($mh, $running) == CURLM_CALL_MULTI_PERFORM); //Запускаем соединения

usleep (100000);  //100мс. Зачем эти две строки? См. "Очень важный момент" в статье
$status = curl_multi_exec($mh, $running);

//Пока есть незавершенные соединения и нет ошибок мульти-cURL
while ($running > 0 && $status == CURLM_OK) {
    $sel = curl_multi_select($mh, 4); //ждем активность на файловых дескрипторах. Таймаут 4сек
    usleep (500000);                 //500мс. Особая хитрость (см. пояснения после примера)

    //Вдруг cURL хочет быть вызвана немедленно опять..
    while (($status = curl_multi_exec($mh, $running)) == CURLM_CALL_MULTI_PERFORM);

    //Если есть завершенные соединения
    while (($info = curl_multi_info_read($mh))! = false) {
        $easyHandle = $info['handle'];    //простой дескриптор cURL
        $one = curl_getinfo($easyHandle); //получаем инфу по каждому простому дескриптору
        $httpCode = $one['http_code'];
        $text = "<br>URL: ${one['url']} | HTTP code: $httpCode";
        $text. = "<br>Total time: <b>${one['total_time']}</b><br>";
        echo "$text<br>"; flush(); //Сразу выводим инфу в браузер
        if ($httpCode == 200) {    //если файл/страница успешно получена
            //... делаем что-нибудь с полученным файлом ...
        }
        curl_multi_remove_handle($mh, $easyHandle);
        curl_close($easyHandle);
    }
}

Пример создан для демонстрации теоретического материала, изложенного выше. Он состоит из двух скриптов: test.php возвращает текущее время. Если ему передан в GET-запросе параметр p=NN, то перед ответом скрипт ждет NN секунд. Это эмуляция разного времени ответа от web-серверов в реальных запросах. Все для того, чтобы показать одновременную работу сURL со всеми заданными адресами. Второй скрипт архива, query.php - собственно сама программа с мульти-cURL.

Если в строке 24 будет значение $sel = 1, значит найден один файловый дескриптор, готовый к передаче. Вполне может быть, что их будет несколько и значение будет больше. Если $sel = 0, значит таймаут истек, но нет ни одного готового дескриптора. Значение $running - количество еще активных простых дескрипторов cURL.

В строке 25 стоит пауза. Это не "по учебнику", я придумал такую хитрость, чтобы дожидаться окончания одновременных запросов. Они обычно отстают друг от друга где-то на 0.5 секунды и такая пауза уменьшает количество итераций основного цикла. На практике она может оказаться бесполезной, можно смело ее убрать.

Функцию curl_multi_info_read() засунул в цикл (строка 31), т.к. есть вероятность, что несколько дескрипторов закончатся одновременно (особенно с паузой выше), тогда их нужно обработать в одном шаге основного цикла.

В примере я ставил таймаут select-а (строка 24) на несколько секунд только потому, что ответ предполагается ждать долго. На практике можно явно не указывать таймаут, тогда он будет равен 1сек. Все зависит от области применения. Одно могу сказать точно, curl_multi_select() нужен в коде. Он заметно разгружает скрипт и ресурсы сервера.

Проблема этого примера в том, что приходится "ловить лису за хвост". Сервер хостера работает в разы шустрее, чем моя машина. Поэтому чтобы понять смысл кода, придется экспериментировать с таймаутом select-а, GET-запросом и паузой после curl_multi_select(). Если вам все это до лампочки, то на практике можно просто использовать код, как есть.

[UPD] Прошло время, и сам не понял ход своих мыслей =) Строки 19-20. Сначала перечитываем "Очень важный момент", там два абзаца. Теперь поясняю: пауза нужна, чтобы пропустить переходное состояние libcurl между статусами *_PERFORM и *_OK, когда библиотека не вернет ни одного дескриптора и значит нечего разбирать в последующем цикле. Вот в чем была идея :) Кроме того, без этой паузы получим зависание на первом шаге цикла в строке 24, пока таймаут, заданный в *_select() не кончится. Все это наблюдалось на медленной машине. Теперь ОЗУ прибавилось, не получается подтвердить слова.

Однако это не все странности :( Специально погонял этот код с отладочными var_dump(). Смотрите строки 17-20 (первый цикл и повторный вызов curl_multi_exec()). Знаете, что будет возвращать curl_multi_exec() на последних шагах цикла? CURLM_CALL_MULTI_PERFORM, затем FALSE (а не CURLM_OK, как ожидалось), что приводит к выходу из цикла. Далее вызов в 20-й строке вернет CURLM_OK, на быстрой машине даже пауза не нужна.

А вот еще одна загадка: в строке 28 прописан тот же самый цикл опроса libcurl, что был в начале (с17). Но после него $status никогда не содержит FALSE, всегда только CURLM_OK! Ну и что, спрашивается, делает библиотека после запуска дескрипторов, раз не может сразу вернуть правильный статус при запуске процесса?


Есть более короткий пример использования мульти-cURL, основанный на первой части статьи. Забьем на всякие ожидания и т.п. и получим следующее (источник + мои доработки):

$urls = array(
   'http://www.google.ru/',
   'http://www.altavista.com/',
   'http://www.yahoo.com/'
    );

$mh = curl_multi_init();

foreach ($urls as $i => $url) {
    $conn[$i]=curl_init($url);
    curl_setopt($conn[$i],CURLOPT_RETURNTRANSFER,1);  //ничего в браузер не давать
    curl_setopt($conn[$i],CURLOPT_CONNECTTIMEOUT,10); //таймаут соединения
    curl_multi_add_handle ($mh,$conn[$i]);
}

//Пока все соединения не отработают
do { curl_multi_exec($mh,$active); } while ($active);

//разбор полетов
for ($i = 0; $i < count($urls); $i++) {
    //ответ сервера в переменную
    $res[$i] = curl_multi_getcontent($conn[$i]); 
    curl_multi_remove_handle($mh, $conn[$i]);
    curl_close($conn[$i]);
}
curl_multi_close($mh);

print_r($res);

[1oo%, EoF]
Похожие материалы: cURL - ключ от всех дверей
Понравилась статья? Расскажите о ней друзьям:

Метки: PHP, кодинг

Комментарии
Для работы модуля комментариев включите javaScript


Показать/скрыть правила
Имя
[i] [b] [u] [s] [url]
:-) ;-) :D *lol* 8-) :-* :-| :-( *cry* :o :-? *unsure* *oops* :-x *shocked* *zzz* :P *evil*

Осталось 1000 символов.
Код защиты от спама Обновить код
Каждый комментарий проходит ручную модерацию. 100% фильтрация спама.
Продвижение
Время
Метки