Eseguire richieste HTTP concorrenti con cURL multi in PHP

by theArchitect
SHARE
Eseguire richieste HTTP concorrenti con cURL multi in PHP
© Guida-HTML5.it

Introduzione

Quando un’applicazione PHP deve interrogare più endpoint HTTP, eseguire le richieste una dopo l’altra può diventare inefficiente. Se ogni richiesta impiega 500 millisecondi e ne dobbiamo effettuare dieci, il tempo totale può avvicinarsi ai 5 secondi, anche quando il server remoto elabora le richieste contemporaneamente.

La soluzione è usare l’estensione cURL in modalità multi, tramite le funzioni curl_multi_init(), curl_multi_add_handle() e curl_multi_exec(). Questo approccio permette di avviare più richieste HTTP nello stesso momento e di raccogliere le risposte quando diventano disponibili.

È utile, per esempio, in una dashboard che deve recuperare dati da diversi servizi, in un aggregatore di contenuti o in uno script che deve interrogare più API indipendenti. Non si tratta di multithreading tradizionale: PHP coordina più trasferimenti cURL attraverso un unico processo.

Codice completo

L’esempio seguente interroga tre endpoint pubblici in parallelo. Per ogni richiesta vengono salvati URL, codice HTTP, contenuto ricevuto ed eventuale errore.

<?php

$urls = [
    ´https://jsonplaceholder.typicode.com/posts/1´,
    ´https://jsonplaceholder.typicode.com/users/1´,
    ´https://jsonplaceholder.typicode.com/todos/1´,
];

// Crea il gestore multi-cURL.
$multiHandle = curl_multi_init();

$handles = [];

// Prepara una singola risorsa cURL per ogni URL.
foreach ($urls as $index => $url) {
    $handle = curl_init($url);

    curl_setopt_array($handle, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_FOLLOWLOCATION => true,
        CURLOPT_CONNECTTIMEOUT => 5,
        CURLOPT_TIMEOUT => 15,
        CURLOPT_HTTPHEADER => [
            ´Accept: application/json´,
        ],
    ]);

    // Aggiunge la richiesta al gestore multi-cURL.
    curl_multi_add_handle($multiHandle, $handle);

    // Conserva l´handle associandolo all´indice originale.
    $handles[$index] = $handle;
}

// Avvia ed esegue tutte le richieste.
$running = null;

do {
    $status = curl_multi_exec($multiHandle, $running);

    if ($status !== CURLM_OK) {
        throw new RuntimeException(
            ´Errore multi-cURL: ´ . curl_multi_strerror($status)
        );
    }

    // Attende un evento di rete per evitare un ciclo occupato.
    if ($running > 0) {
        $selected = curl_multi_select($multiHandle, 1.0);

        if ($selected === -1) {
            // Evita un busy loop quando il sistema non segnala eventi.
            usleep(10000);
        }
    }
} while ($running > 0);

// Legge e stampa i risultati.
$results = [];

foreach ($handles as $index => $handle) {
    $body = curl_multi_getcontent($handle);
    $curlError = curl_error($handle);
    $httpCode = curl_getinfo($handle, CURLINFO_HTTP_CODE);

    $results[$index] = [
        ´url´ => $urls[$index],
        ´status´ => $httpCode,
        ´body´ => $body,
        ´error´ => $curlError !== ´´ ? $curlError : null,
    ];

    // Rimuove la richiesta dal gestore multi.
    curl_multi_remove_handle($multiHandle, $handle);
    curl_close($handle);
}

curl_multi_close($multiHandle);

// Visualizza un riepilogo leggibile.
foreach ($results as $result) {
    echo ´URL: ´ . $result[´url´] . PHP_EOL;
    echo ´HTTP status: ´ . $result[´status´] . PHP_EOL;

    if ($result[´error´] !== null) {
        echo ´Errore cURL: ´ . $result[´error´] . PHP_EOL;
    } else {
        echo ´Risposta: ´ . $result[´body´] . PHP_EOL;
    }

    echo str_repeat(´-´, 50) . PHP_EOL;
}

Spiegazione

Ogni chiamata a curl_init() crea un handle indipendente. L’handle contiene la configurazione di una singola richiesta: URL, timeout, header e modalità di restituzione del corpo.

Con curl_multi_init() viene creato un contenitore capace di coordinare più handle. La funzione curl_multi_add_handle() inserisce le richieste nel contenitore, mentre curl_multi_exec() avvia l’esecuzione e aggiorna il numero di richieste ancora attive attraverso la variabile $running.

Il ciclo continua finché $running è maggiore di zero. La funzione curl_multi_select() sospende temporaneamente l’esecuzione in attesa di attività di rete. È preferibile rispetto a un ciclo che richiama continuamente curl_multi_exec(), perché riduce il consumo della CPU.

Quando tutte le richieste sono terminate, curl_multi_getcontent() recupera il corpo della risposta. È importante controllare sia l’errore cURL, tramite curl_error(), sia il codice HTTP ottenuto con curl_getinfo(). Una richiesta può infatti concludersi senza errore tecnico ma restituire, per esempio, un codice 404 o 500.

Infine, ogni handle deve essere rimosso con curl_multi_remove_handle() e chiuso con curl_close(). Anche il gestore principale deve essere chiuso con curl_multi_close(), così da liberare correttamente le risorse.

Best practice

  • Limita il numero di richieste simultanee: non aggiungere migliaia di handle in una sola volta. Per grandi volumi, usa una coda e processa i lavori a gruppi.
  • Imposta sempre i timeout: CURLOPT_CONNECTTIMEOUT limita il tempo per stabilire la connessione, mentre CURLOPT_TIMEOUT limita la durata complessiva.
  • Controlla il codice HTTP: considera corretti principalmente i codici tra 200 e 299, ma gestisci esplicitamente anche 3xx, 4xx e 5xx.
  • Non disabilitare la verifica TLS: evita di usare CURLOPT_SSL_VERIFYPEER => false in produzione. La verifica del certificato protegge da attacchi man-in-the-middle.
  • Conserva l’associazione tra handle e URL: le richieste possono terminare in ordine diverso da quello di avvio. Gli indici dell’esempio permettono di collegare ogni risposta alla richiesta corretta.
  • Gestisci le risposte parziali o inattese: verifica che il corpo non sia vuoto e che il formato ricevuto corrisponda a quello atteso prima di elaborarlo.
  • Rispetta i limiti dei servizi remoti: richieste parallele troppo numerose possono causare rate limit, blocchi temporanei o sovraccarico del servizio.

Riepilogo

cURL multi consente di eseguire più richieste HTTP concorrenti senza attendere il completamento di ciascuna richiesta prima di avviare la successiva. Il procedimento consiste nel creare gli handle, aggiungerli a un gestore multi, eseguire il ciclo con curl_multi_exec(), leggere i risultati e liberare le risorse.

Questo approccio riduce sensibilmente i tempi quando le richieste sono indipendenti e attendono soprattutto la rete. Deve però essere usato con attenzione: timeout, codici HTTP, errori, limiti di concorrenza e verifica TLS sono elementi fondamentali di un’implementazione affidabile.

Approfondisci con risorse ufficiali

  • Documentazione PHP su curl_multi_init(): descrive la creazione di un gestore per più trasferimenti cURL.
  • Documentazione PHP su curl_multi_exec(): spiega l’esecuzione e il controllo delle operazioni concorrenti.
  • Documentazione PHP su curl_multi_select(): illustra come attendere gli eventi di rete evitando cicli inutilmente intensivi.
  • Documentazione ufficiale libcurl: offre dettagli sul funzionamento interno delle interfacce multi e sulle opzioni avanzate.

SHARE