Статьи
"Многопоточность" в PHP (curl)
Использование библиотеки curl.
Эта статья является первой из серии "Многопоточность" в PHP
Curl – это библиотека, позволяющая подсоединяться к разным серверам по разным протоколам. Обладает удобством в работе и способностью гибко настраиваться.
Curl реализует механизм множественных запросов, или мультизапросов. Его принцип заключается в том, что посылается несколько запросов, при этом перед отправкой следующего не ожидается ответ на предыдущий.
Используем это в нашем примере скачивания нескольких страниц.
Рассмотрим сначала процесс скачивания содержимого с одного url.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php $url = 'mail.ru'; // инициализация сеанса curl $ch = curl_init('http://'.$url); // curl_exec будет возвращать результат curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // не будет возвращаться http-заголовок curl_setopt($ch, CURLOPT_HEADER, 0); // загрузка страницы и выдача её браузеру $content = curl_exec($ch); // завершение сеанса и освобождение ресурсов curl_close($ch); ?> |
Здесь функцией curl_init мы инициализируем сеанс curl и в качестве параметра передаем урл страницы, которую хотим скачать. Далее первым вызовом функции curl_setopt говорим, что результат надо вернуть, а не вывести в браузер, и вторым запрещаем передачу нам http-ответа сервера. curl_setopt принимаюет в качестве параметров дескриптор соединения $ch, название опции и ее значение соответственно. С помощью curl_setopt можно задать много параметров для более тонкого управления соединением, подробнее читайте в мануале к этой библиотеке. Затем функцией curl_exec производим собственно скачивание и в завершении закрываем соединение - curl_close. В переменной $content у нас теперь находится код страницы, указанной в $url.
А сейчас давайте попробуем скачать сразу несколько страниц (основа примера взята из документации на php.net):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 |
<?php // страницы, содержимое которых надо получить $urls = array('yandex.ru', 'google.ru', 'mail.ru', 'rambler.ru'); // инициализируем "контейнер" для отдельных соединений (мультикурл) $cmh = curl_multi_init(); // массив заданий для мультикурла $tasks = array(); // перебираем наши урлы foreach ($urls as $url) { // инициализируем отдельное соединение (поток) $ch = curl_init('http://'.$url); // если будет редирект - перейти по нему curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // возвращать результат curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // не возвращать http-заголовок curl_setopt($ch, CURLOPT_HEADER, 0); // таймаут соединения curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 10); // таймаут ожидания curl_setopt($ch, CURLOPT_TIMEOUT, 10); // добавляем дескриптор потока в массив заданий $tasks[$url] = $ch; // добавляем дескриптор потока в мультикурл curl_multi_add_handle($cmh, $ch); } // количество активных потоков $active = null; // запускаем выполнение потоков do { $mrc = curl_multi_exec($cmh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); // выполняем, пока есть активные потоки while ($active && ($mrc == CURLM_OK)) { // если какой-либо поток готов к действиям if (curl_multi_select($cmh) != -1) { // ждем, пока что-нибудь изменится do { $mrc = curl_multi_exec($cmh, $active); // получаем информацию о потоке $info = curl_multi_info_read($cmh); // если поток завершился if ($info['msg'] == CURLMSG_DONE) { $ch = $info['handle']; // ищем урл страницы по дескриптору потока в массиве заданий $url = array_search($ch, $tasks); // забираем содержимое $tasks[$url] = curl_multi_getcontent($ch); // удаляем поток из мультикурла curl_multi_remove_handle($cmh, $ch); // закрываем отдельное соединение (поток) curl_close($ch); } } while ($mrc == CURLM_CALL_MULTI_PERFORM); } } // закрываем мультикурл curl_multi_close($cmh); ?> |
Код подробно откомментирован, но давайте разберем все по порядку.
В 7 строчке мы инициализируем контейнер для отдельных соединений curl (далее я буду называть его мультикурл), именно он позволит нам проводить операции с ними параллельно.
Далее в цикле инициализируем соединение (назовем его поток) для каждого урла из нашего массива, попутно добавляя его в мультикурл и в массив заданий $tasks. $tasks – массив, в котором ключами являются адреса наших страниц, а значениями – соответствующие дескрипторы curl. Функция curl_multi_add_handle добавляет к дескриптору нашего мультизапросного соединения отдельное созданное соединение.
В 34 строке запускается цикл для начала работы нашего мультикурла. Функция curl_multi_exec одновременно отправляет на выполнение все объявленные потоки, при этом в переменную $active заносится количество выполняемых потоков.
В основном цикле, начинающемся на 40 строчке, происходят главные действия. Он выполняется до тех пор, пока есть незавершенные потоки или пока не произошла ошибка. В 42 строке вызывается функция curl_multi_select, которая проверяет готовность какого-либо из потоков к дальнейшим действиям с ним. Затем, в 47 строке, функцией curl_multi_info_read получаем информацию о потоке. Но так как curl_multi_info_read обновляет возвращаемую информацию только после вызова curl_multi_exec, сделаем это в строке 45.
Функция curl_multi_info_read возвращает массив, в котором нас интересуют ключи 'msg' и 'handle'. По 'msg' мы проверяем, выполнился ли поток, а по 'handle' узнаем его дескриптор. Получив дескриптор, ищем по массиву заданий, к какому урлу он относится и записываем вместо него вожделенное содержимое страницы, получаемое функцией curl_multi_getcontent.
Теперь, когда данный поток выполнил свою задачу, удаляем его из мультикурла, а потом закрываем самого.
После завершения всех потоков функцией curl_multi_close закрываем мультикурл.
Сейчас в массиве заданий $tasks находятся html-коды заданных страниц.
Надеюсь, после прочтения статьи стало немного понятнее, как можно реализовать выполнение "многопоточных" запросов на PHP.
В следующей статье рассмотрим использование stream-функций для подобных целей.
|
|
0 | Tweet | Нравится |
|
На эту статью оставлено 47 комментариев
-
-
-
-
bill
26 Март 2010 19:05:37 (ссылка)а подскажите может, как найти тот url который не получилось захватить? Как например повторить запрос если не удалось, по вышеприведённой методике делаю проверку прокси, там идет не список урл а список прокси, так вот если не возвращает страничку значит прокси не работает.
Посоветуюте может начать учит сокеты, даже не знаю как быть
-
Pablo Monteagudo
28 Март 2010 23:33:31 (ссылка)2 bill
Не знаю, правильно ли я Вас понял...
>а подскажите может, как найти тот url который не получилось захватить?
Тогда в конце выполнения скрипта в массиве $tasks по этому урлу будет не та информация, которую Вы ожидаете) В процессе же выполнения можете проверять, что возвращает функция curl_multi_getcontent.Нужно ли учить сокеты именно в Вашем случае, не знаю. Для проверки прокси в принципе достаточно использования курла.
-
-
-
Тимур
01 Апрель 2010 14:49:56 (ссылка)Здравствуйте.
Спасибо за статью. У меня есть несколько вопросов.
1) Цикл в 34 строке делает различное количество итераций (не понимаю от чего это зависит, просто попробовал запустить несколько раз скрипт). Почему? Зачем вообще организовывать этот цикл? Функция curl_multi_exec не может выполнить все запросы за один раз?
2) Ладно, допустим все запросы выполнены. Что делает функция curl_multi_select? Проверял на Вашем примере, она никогда не возвращала -1, т.е. условная конструкция в 42 строке вообще не нужна?
3) Для чего в 44 строке опять организовывать цикл? Ведь мы уже вызывали curl_multi_exec. Получается что опять выполняется запрос?
Хочется во всем этом разобраться. Пока вся это мультикурловщина кажется не логичной/криво реализованной. Спасибо, жду ответа.
-
Pablo Monteagudo
01 Апрель 2010 19:16:48 (ссылка)2 Тимур
А я-то наивно думал, что статья все подробно объясняет
1) Ну вот смотрите первый пример, без мультикурла. Там выполняется ф-ия curl_exec. Она может выполняться раз от раза разное время, надо подконнектиться к серверу, отправить информацию, считать информацию... А теперь возьмите ф-ию curl_multi_exec, которая запускает наши дескрипторы. И как раз в цикле мы следим, когда этот процесс закончится. Как и в первом случае он будет выполняться разное время, отсюда и разное количество итераций.
2) >она никогда не возвращала -1
Хорошо, что не возвращала, т.к. -1 это признак ошибки.
Про эту ф-ию можно почитать здесь - http://www.php.net/manual/en/function.curl-multi-select.php , особенно обратите внимание на комментарий. В двух словах, она смотрит, есть ли какая-то активность в наших соединениях и проверяет возникшие ошибки. В принципе, можно обойтись вообще без нее (пример ниже).3) Этот цикл такой же, как и в 34 строке. Вот так наверно будет понятнее (сокращенный основной цикл):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
while ($active && ($mrc == CURLM_OK)) { do { $mrc = curl_multi_exec($cmh, $active); } while ($mrc == CURLM_CALL_MULTI_PERFORM); $info = curl_multi_info_read($cmh); if ($info['msg'] == CURLMSG_DONE) { $ch = $info['handle']; $url = array_search($ch, $tasks); $tasks[$url] = curl_multi_getcontent($ch); curl_multi_remove_handle($cmh, $ch); curl_close($ch); } }
Мы должны постоянно запускать curl_multi_exec, чтобы отслеживать, какие из дескрипторов выполнили свою задачу.
-
-
-
-
-
-
Konstantine
12 Май 2010 04:29:08 (ссылка)Статья помогла разобраться с многопоточностью, спасибо. Думаю очень актуально в сервисах использовать данную реализацию, скорость выростает в разы. Примерно пол года назад пытался изучить многопоточность php но не хватило знаний, сейчас уже дорос и освоил.
-
Andrey
14 Август 2010 20:02:31 (ссылка)Здравствуйте! Мне надо сделать следующее: скрипт проходит авторизацию на сайте, открывается страница профиля. А потом сразу же скрипт должен перейти на другую страницу этого же сайта. Авторизацию я прошёл, на страницу профиля попал, куки сохранил. А что дальше делать никак не соображу.
-
-
Балбес
23 Август 2010 09:24:46 (ссылка)Спасибо. Отличная работа. А как скрипт немного модифицировать? Надо собрать контент с 3 сайтов. С каждого будет собираться много страниц(www/site1/?p=1, www.site1/?p=2) и т.д. Задача собирать контент так, чтобы с каждый сайт обрабатывался соответствующим потоком. Т.е. чтобы не получилось так, что одновременно скрипт начнет собирать с 3 сайтов контент( site1.ru, site2.ru, site3.ru), с site2 собирет быстрее и на его место поставит следующий из задания, т.е. site1/?p=2 и получится так, что одновременно в 2 потока парсится один и тот же сайт. Реально ли таккую задачу осуществить?
-
Pablo Monteagudo
23 Август 2010 23:36:05 (ссылка)То есть вы хотите скачивать с каждого сайта страницы последовательно, но чтобы вместе эти сайты парсились одновременно? Если сайта только 3, то по-моему нет смысла усложнять код, достаточно просто запустить 3 копии скрипта с обычным последовательным скачиванием страниц для каждого сайта.
А для модификации скрипта на вскидку надо сделать следующее.
Основной цикл, начинающийся в 40-й строке поместить в еще один, который на каждой итерации помещает в массив $tasks три задания. Задания представляют собой массив урлов каждого сайта, что-нибудь такое1 2 3 4 5 6
$common_tasks = array( 0 => array('site1' => '?p=1', 'site2' => '?p=1', 'site3' => '?p=1'), 1 => array('site1' => '?p=2', 'site2' => '?p=2', 'site3' => '?p=2'), 2 => array('site1' => '?p=3', 'site2' => '?p=3', 'site3' => '?p=3'), .......................... );
В итоге алгоритм будет представлять собой нечто типа
1 2 3 4 5 6 7
цикл1 { берем очередной элемент массива $common_tasks; помещаем его элементы в массив заданий для мультикурла ($tasks); цикл2 { работа мультикурла; } }
-
Дмитрий
19 Сентябрь 2010 17:25:42 (ссылка)Скажите, а на сколько этот код в действительности многопоточный, насколько я понимаю экономия времени здесь происходит только засчет открытия-закрытия сокетов(неблокировка), то есть их подготовка, а сама основная часть, то есть запись-чтение непосредственно контента в/из сокета все равно последовательна..
Может есть смысл попробовать решения на подобии http://habrahabr.ru/blogs/php/40245/.
И их как-то сравнить, но там наверняка много всяких тонкостей.
-
Pablo Monteagudo
26 Сентябрь 2010 20:00:11 (ссылка)Чтение/запись также непоследовательны. При данных операциях не происходит ожидание ответа от сервера, а идет дальнейшая работа скрипта.
По ссылке изложен механизм работы, который по своей сути не отличается от курловского, смотрите статьи http://jo-in.ru/articles/post/92 , http://jo-in.ru/articles/post/194 -
Андрей
01 Ноябрь 2010 09:18:55 (ссылка)Подскажите, пожалуйста, как в multi curl настроить паузу между отправкой запросов на удаленный сервер, чтобы распределить его нагрузку во времени. Имеется в виду не управление количеством одновременных сессий, а именно настройка паузы между запросами.
-
Pablo Monteagudo
01 Ноябрь 2010 14:30:48 (ссылка)А зачем использовать мультикурл, если нужны паузы между отдельными запросами?
Если же нужны паузы между "пачками" запросов, то можно сделать так. Заключаем весь код из примера в цикл, в начале которого формируется массив $urls. Например есть массив $all_urls из 100 урлов. В начале цикла создаем массив $urls из первых 10 урлов массива $all_urls. После выполнения операций мультикурла ставим задержку. В массиве $all_urls осталось 90 элементов, из них опять формируем массив $urls и так далее. -
Eugene S
21 Декабрь 2010 14:18:58 (ссылка)При закачке в несколько "потоков" (10) 100 фото порою не все фото закачивались. Этот подход бесконечного цикла сработал http://www.php.net/manual/en/function.curl-multi-exec.php#88453 .
-
Sasha
25 Февраль 2011 15:41:43 (ссылка)Добавил proxy и user_agent.
Строка:1
curl_setopt($ch, CURLOPT_USERAGENT, $uagent);
Выдает: 400 Bad Request.
1
curl_setopt($ch, CURLOPT_PROXY, $proxy);
Выдоает произвольный обрывок.
И еще если бы вы показали как подставлять другой прокси, если предыдущий dead..
-
-
DaeWoo
28 Февраль 2011 18:04:36 (ссылка)А можно как либо передать пост запрос в несколько потоков с помощью курл?
Допустим имеется такой код, возможно ли переделать его под многопоточность?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
$old = c("edit4")->text; $next = c("edit5")->text; $comment = c("memo1")->text; $login = c("edit2")->text; $pass = c("edit3")->text; $xy = rand(0,30); $url = c("edit1")->text; for($i=$old;$i<$next;$i++) { $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "$url/index.php"); curl_setopt ($ch, CURLOPT_POST, 1); curl_setopt ($ch, CURLOPT_POSTFIELDS, "login_name=$login&login_password=$pass&login=submit&image.x=$xy&image.y=$xy"); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIESESSION, TRUE); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt ($ch, CURLOPT_COOKIEJAR, 'cook.txt'); curl_setopt ($ch, CURLOPT_COOKIEFILE, 'cook.txt'); $result = curl_exec ($ch); curl_close ($ch); $ch = curl_init(); curl_setopt($ch, CURLOPT_URL, "$url/engine/ajax/addcomments.php"); curl_setopt ($ch, CURLOPT_POST, 1); curl_setopt ($ch, CURLOPT_POSTFIELDS, "comments=$comment&name=$login&mail=&skin=Default&allow_subscribe=0&post_id=$i&rndval=1298206420478"); curl_setopt ($ch, CURLOPT_RETURNTRANSFER, 1); curl_setopt($ch, CURLOPT_COOKIESESSION, TRUE); curl_setopt($ch, CURLOPT_HEADER, 0); curl_setopt ($ch, CURLOPT_COOKIEJAR, 'cook.txt'); curl_setopt ($ch, CURLOPT_COOKIEFILE, 'cook.txt'); $result_pm = curl_exec ($ch); curl_close ($ch); print_r("Добавлено в id=$i "); ///print_r($result_pm); }
-
-
-
Joe
14 Июнь 2011 22:30:48 (ссылка)Вот так намного понятнее, чем "миллиард" циклов всяких:
do {
$mrc = curl_multi_exec($cmh, $active);
$info = (object) curl_multi_info_read( $cmh );
$ready = curl_multi_select( $cmh );if( $info->msg == CURLMSG_DONE )
{
if( ! ($error = $info->result) ) {
echo curl_multi_getcontent( $info->handle );
}
}
}
while( $mrc == CURLM_CALL_MULTI_PERFORM || $active > 0 ); -
-
Russin
07 Июль 2011 22:40:50 (ссылка)А кто нибудь подскажет если я перебираю в цикле около 150ссылок по первому примеру в этой статье то у меня постоянно разный результат получается т.е. получается что в цикле информация вытягивается 10-15 ссылок И ВСЕ!! а остальные почему не обрабатываются?? что это может быть?? или так делать нельзя???)))
-
-
-
Антон
14 Октябрь 2011 14:33:49 (ссылка)Спасибо за статью!
У меня вопрос такого характера. Мне нужно отловить любые ошибки в процессе работы этого скрипта. Например, я получаю контент с 5 сайтов. И если произошел сбой каком-то соединении любого характера(будь то таймаут, ожидание или ошибка), мне нужно например отправить мыло с характером ошибки и какой url отвалился. Не подскажите, в каких местах мне эти проверки дописать? Заранее очень благодарен за помощь! -
-
Антон
19 Октябрь 2011 12:18:58 (ссылка)Еще такой вопрос. Например, если поставить timeout 1 сек , то после того, как не выполняется 1 curl цикл прерывается, а не продолжается дальше! Понятно, желательно, чтобы так не было. Нужно чтобы цикл продолжался, независимо от одного необработанного url. Как можно модифицировать?
-
-
Антон
09 Ноябрь 2011 13:11:54 (ссылка)А еще такой вопрос. Вот пример кода
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
// выполняем, пока есть активные потоки while ($active && ($mrc == CURLM_OK)) { // если какой-либо поток готов к действиям if (curl_multi_select($cmh) != -1) { // ждем, пока что-нибудь изменится do { $mrc = curl_multi_exec($cmh, $active); // получаем информацию о потоке $info = curl_multi_info_read($cmh); // если поток завершился if ($info['msg'] == CURLMSG_DONE) { $ch = $info['handle']; $chinfo = curl_getinfo($ch); //если сервер на ответил, шлем письмо с url сайта if (!$chinfo['http_code']) { ..............
и в этом месте, в случае если нет http_code, ставить данный url опять в очередь? пока все-таки не получишь ответ.
-
Pablo Monteagudo
09 Ноябрь 2011 13:38:07 (ссылка)http_code - это код состояния http, посылаемый сервером. Почитать про это можно, например, здесь - http://job-interview.ru/articles/post/86 или здесь - http://ru.wikipedia.org/wiki/%D0%A1%D0%BF%D0%B8%D1%81%D0%BE%D0%BA_%D0%BA%D0%BE%D0%B4%D0%BE%D0%B2_%D1%81%D0%BE%D1%81%D1%82%D0%BE%D1%8F%D0%BD%D0%B8%D1%8F_HTTP А что именно вам надо проверять - удачный ли ответ или еще что - зависит от ваших целей.
-
Антон
09 Ноябрь 2011 13:46:56 (ссылка)я знаю, что это код состояния, и в некоторых случаях он приходит пустым, вернее значение $chinfo['http_code'] пустая строка. Тестирую на 5-6 сайтах, и по непонятным мне причинам, любой из сайтов может вот так вот отвалиться. Соответственно, и нормального ответа не приходит. Скрипт отрабатывает в среднем за 1,5 сек. таймауты стоят по 10 сек. Т.е. времени хватает. В чем может быть дело?
-
-
Антон
09 Ноябрь 2011 19:24:22 (ссылка)Так вот я и спрашивал вас, каким образом его можно опять поставить в поток. У нас же далее идет
1 2 3
curl_multi_remove_handle($cmh, $ch); // закрываем отдельное соединение (поток) curl_close($ch);
. Можно ли после этого каким-то образом заново вставить этот $ch в multi_curl? Спасибо за ответы
-
-
-
Юрий
29 Февраль 2012 20:26:58 (ссылка)Может я что-то не понимаю..
К примеру у нас стоит time limit 30 сек. на выполение скрипта
если у меня будет в $urls 1000 страниц которые нужно открыть это значит, что к примеру на 100 странице все закончится ошибкой? как сделать чтобы перебрать это страницы все? -






