Le Hollandais Volant

[PHP] Faire plusieurs requêtes HTTP simultanées avec cURL

two elephants Toujours en développement de mon lecteur RSS en PHP, je me suis heurté au problème de la mise à jour des 154 flux auquel je suis abonné. Si le file_get_contents() fonctionne avec les URL, il ne peut les télécharger que les uns après les autres. Ainsi, même en ne mettant qu’un délai d’attente d’une seconde, la mise à jour prendrait au minimum 2 minutes 34 secondes. C’est bien trop long.

Une solution aurait pu être de faire 154 requêtes depuis les navigateur, pour ouvrir 154 instances de file_get_contents(), mais ça me semblait trop lourd.

La meilleure solution est à mon avis d’utiliser le module curl : lui, il peut faire 154 requêtes simultanées, et la durée de l’opération totale sera simplement la durée de la plus longue des requêtes et non la somme de toutes les requêtes. Avec cette méthode, je met à jour tous mes flux en moins d’une minute, toutes opérations incluses.

En pratique, pour récupérer le contenu de plusieurs URL, ça se présente comme suit.

On initialise une liste de sessions curl « $multihandler » : chacune correspondra à une instance de curl qui ira se connecter à une URL :
// init multi handler
$multihandler = curl_multi_init();
$handlers = $result = array();

On peuple la liste des sessions : chaque session est initialisée individuellement (avec une option CURLOPT_RETURNTRANSFER qui force curl à retourner le contenu plutôt que de l’afficher) puis on ajoute la session $handler[$i] à la liste des sessions $multihandler
// init each url
foreach ($urls as $i) {
	$handlers[$i] = curl_init($i);
	curl_setopt($handlers[$i], CURLOPT_RETURNTRANSFER, TRUE);
	curl_multi_add_handle($multihandler, $handlers[$i]);
}

Il n’y a plus qu’à établir toutes les connexions aux URL et télécharger les données.
Le $pendingConnex est une variable donnée par curl qui donne le nombre de connexions actives. Ce nombre diminuera progressivement jusqu’à zéro quand toutes les URL auront été téléchargées.

Les 10 ms d’attente sont là pour éviter que PHP ne fasse trop de tests, et permet de réduire la charge serveur (si on ne le met pas, la charge serveur explose — une méthode plus propre, mais dont le gain par rapport au sleep() n’est pas énorme est visible dans la doc).
10ms, pour un temps d’exécution de 10 seconde sur un processeur qui fait une boucle par nanoseconde (proc à 1 GHz) permet de réduire le nombre d’itérations par un facteur 10'000'000, ce qui est colossal : le serveur a dix millions de fois moins de puissance à allouer.

Dans mon lecteur RSS, je mets dans cette boucle do/while le code qui m’affiche à intervalle régulier le nombre de requêtes restantes.

// exec connexions + download
do {
	curl_multi_exec($multihandler, $pendingConnex);
	usleep(10000); // 10 ms
} while ($pendingConnex > 0);

À ce stade, les requêtes sont faites, les données récupérées en mémoire.
Il suffit de les parser et de les récupérer dans le tableau $result. Ce tableau contiendra les données en valeur et les URL en clés :

// parse responses
foreach ($urls as $i) {
	$result[] = curl_multi_getcontent($handlers[$i]);
}

Le code ci-dessus devrait être suffisant.
Néanmoins, je conseille d’ajouter quelques options, avec curl_setopt() :

  • Pour suivre les redirections 302 et 301 de façon transparentes :
    curl_setopt($handlers[$i], CURLOPT_FOLLOWLOCATION, TRUE);
  • Pour réduire la durée d’attente de connexion à 10 secondes :
    curl_setopt($handlers[$i], CURLOPT_CONNECTTIMEOUT, 10);
  • Pour réduire la durée d’attente de téléchargement des données à 30 secondes
    curl_setopt($handlers[$i], CURLOPT_TIMEOUT, 30);
  • Pour accepter les cookies de sessions :
    curl_setopt($handlers[$i], CURLOPT_COOKIESESSION, TRUE);
  • Pour accepter et décoder les contenus compressés :
    curl_setopt($handlers[$i], CURLOPT_ENCODING, "gzip");

image de Jim Frost