Segunda-feira, 29 de junho de 2009 às 10h30

Non-blocking asynchronous requests usando curl_multi e php

Faltam 0 dias! Inscreva-se agora! O maior encontro de profissionais web da américa latina.

Outro dia eu precisei implementar um script que, ao receber alguns dados enviados por POST pelos usuários do site, faz uma requisição http a uma api externa usando parte desses dados postados. Como as informações que essa api retorna não precisam ser exibidas para o usuário, e como essa api geralmente leva cerca de 2 segundos pra responder, para não deixar o usuário "pendurado" esperando, resolvi que faria uma requisição assíncrona não-bloqueante para um outro script que, por sua vez, acessaria a api e iria tratar/salvar os dados que eu necessitava.

Como o php não tem suporte a threads, a minha solução foi implementada com a biblioteca curl, mais especificamente com a função curl_multi_*(), que permite fazer requisições paralelas e assíncronas. Porém, os exemplos que encontrei tanto na documentação no php.net quanto em classes disponibilizadas por terceiros não funcionavam exatamente do jeito que eu queria, e acabei quebrando a cabeça por algumas horas para encontrar a solução, que gostaria de compartilhar aqui.

A abordagem quase unânime para uso do multi_curl com as quais me deparei propunham o uso dela para fazer as requisições paralelas não bloqueantes, executar em seguida algum processamento não relacionado às requisições (por exemplo, fazer um select no banco de dados), e depois ficar em busy-waiting até que todas as requisições recebam uma resposta, para então finalizar o script. Um exemplo de como fazer isso seria:

// Inicializa um multi-curl handle
$mch = curl_multi_init();
// Inicializa e seta as opções para cada requisição
$ch1 = curl_init('http://www.yahoo.com');
curl_setopt($ch1, CURLOPT_RETURNTRANSFER);
$ch2 = curl_init('http://www.google.com');
curl_setopt($ch2, CURLOPT_RETURNTRANSFER);
// Adiciona a requisição $ch1 ao multi-curl handle $mch.
curl_multi_add_handle($mch, $ch1);

// Executa requisição multi-curl e retorna imediatamente.
curl_multi_exec($mch, $active);

// Repete o procedimento para a requisição $ch2
curl_multi_add_handle($mch, $ch2);
curl_multi_exec($mch, $active);

// Executa outros processamentos

// Fica em busy-waiting até que todas as requisições retornem
do{
curl_multi_exec($mch, $active);
}while($active > 0);

// Acessa as respostas das requisições
$resp1 = curl_multi_getcontent($ch1);
$resp2 = curl_multi_getcontent($ch2);

No meu caso, como eu não precisava das respostas, inicialmente tentei usar este mesmo código, removendo o "do { } while ( )" e as chamadas à curl_multi_getcontent(), isso porque tanto o manual do php.net quanto os textos que li dão a entender que uma única chamada a curl_multi_exec() seria suficiente pra iniciar as requisições assíncronas e retornar imediatamente. Não foi bem o que aconteceu com o meu script, que só fazia a requisição de fato quando eu deixava o curl_multi_exec() em loop, o que não adiantava pra mim, pois o loop só finalizava depois que a mesma retornava uma resposta.

Eis que, depois de algumas horas pesquisando, encontrei o seguinte comentário: "curl_multi_perform is asynchronous. It will only execute as little as possible and then return back control to your program. It is designed to never block. If it returns CURLM_CALL_MULTI_PERFORM you better call it again soon, as that is a signal that it still has local data to send or remote data to receive."

Assim, pra resolver o meu problema bastou substituir a chamada única à curl_multi_exec e o loop que ficava em busy-waiting enquanto a conexão estivesse ativa por um loop que faz chamadas a curl_multi_exec somente enquanto a constante CURLM_CALL_MULTI_PERFORM estivesse retornando TRUE. No caso, o código pra fazer isso seria:

do {
$res = curl_multi_exec($mch, $active);
} while ($res == CURLM_CALL_MULTI_PERFORM);

Deste modo, a requisição é feita (nos meus testes, três iterações deste loop foram suficientes para concluir o envio) e rapidamente retorna o controle para o script, que pode ser finalizado sem esperar por uma resposta, deixando todo mundo (o usuário, o programador e o servidor) feliz :)

P.S.: enquanto pesquisava uma solução, me deparei com algumas classes e funções interessantes para usar o curl_multi. Uma delas está neste post, que contém a implementação eficiente de uma função para quem precisar fazer grande número de requisições em paralelo e processar os resultados à medida que forem retornando. Vai me ser útil num futuro próximo.

Nenhum comentário até agora

Cancelar resposta

Qual a sua opinião?

Faça login abaixo ou cadastre-se rapidamente.


Patrocínio:
Sobre o Autor
Diego Sana mora em Vitória-ES, é empreendedor e desenvolvedor na internet há 10 anos, co-fundador do Flogao.com.br e (quase) bacharel em Ciência da Computação pela UFES. www.sanainside.com

2001 - iMasters FFPA Informática Ltda - Todos os direitos reservados.