Использование библиотеки 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-функций для подобных целей.