Como fazer solicitações HTTP assíncronas em PHP


209

Existe uma maneira no PHP de fazer chamadas HTTP assíncronas? Não me preocupo com a resposta, só quero fazer algo assim file_get_contents(), mas não espere a solicitação terminar antes de executar o restante do meu código. Isso seria super útil para desencadear "eventos" de um tipo no meu aplicativo ou desencadear processos longos.

Alguma ideia?


9
uma função - 'curl_multi', procure nos documentos php por ela. Deve resolver os seus problemas
James Butler

22
O título desta postagem é enganoso. Eu vim procurando chamadas verdadeiramente assíncronas semelhantes às solicitações no Node.js ou AJAX. A resposta aceita não é assíncrona (bloqueia e não fornece retorno de chamada), apenas uma solicitação síncrona mais rápida. Considere mudar a pergunta ou a resposta aceita.
Johntron 29/07/2013

Jogar com o manuseio da conexão via cabeçalhos e buffer não é à prova de balas. Acabo de colocar um novo independente resposta do sistema operacional, navegador ou PHP verison
RafaSashi

1
Assíncrono não significa que você não se importa com a resposta. Significa apenas que a chamada não bloqueia a execução do encadeamento principal. Assíncrono ainda requer uma resposta, mas a resposta pode ser processada em outro encadeamento de execução ou posteriormente em um loop de eventos. Esta pergunta está solicitando uma solicitação de acionar e esquecer que pode ser síncrona ou assíncrona, dependendo da semântica da entrega de mensagens, se você se preocupa com o pedido da mensagem ou com a confirmação da entrega.
precisa saber é o seguinte

Eu acho que você deve fazer essa solicitação HTTP de incêndio no modo sem bloqueio (w / c é o que você realmente deseja) .. Porque quando você chama um recurso, basicamente quer saber se alcançou o servidor ou não (ou por qualquer motivo, você simplesmente precisa da resposta). A melhor resposta é realmente fsockopen e definir a leitura ou gravação do fluxo para o modo sem bloqueio. É como ligar e esquecer.
KiX Ortillan

Respostas:


42

A resposta que eu aceitei anteriormente não funcionou. Ainda esperava respostas. No entanto, isso funciona, retirado de Como faço uma solicitação GET assíncrona em PHP?

function post_without_wait($url, $params)
{
    foreach ($params as $key => &$val) {
      if (is_array($val)) $val = implode(',', $val);
        $post_params[] = $key.'='.urlencode($val);
    }
    $post_string = implode('&', $post_params);

    $parts=parse_url($url);

    $fp = fsockopen($parts['host'],
        isset($parts['port'])?$parts['port']:80,
        $errno, $errstr, 30);

    $out = "POST ".$parts['path']." HTTP/1.1\r\n";
    $out.= "Host: ".$parts['host']."\r\n";
    $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
    $out.= "Content-Length: ".strlen($post_string)."\r\n";
    $out.= "Connection: Close\r\n\r\n";
    if (isset($post_string)) $out.= $post_string;

    fwrite($fp, $out);
    fclose($fp);
}

67
Isso NÃO é assíncrono! Em particular, se o servidor do outro lado estiver inativo, esse trecho de código será interrompido por 30 segundos (o quinto parâmetro no fsockopen). Além disso, o fwrite levará um bom tempo para executar (que você pode limitar com stream_set_timeout ($ fp, $ my_timeout). O melhor que você pode fazer é definir um tempo limite baixo no fsockopen para 0,1 (100ms) e $ my_timeout para 100ms . Você corre o risco, porém, que o tempo limite do pedido.
Chris Cinelli

3
Garanto que é assíncrono e não leva 30 segundos. Isso é um tempo limite máximo. É possível que suas configurações sejam diferentes, causando esse efeito, mas isso funcionou muito bem para mim.
Brent

11
@UltimateBrent Não há nada no código que sugira que seja assíncrono. Ele não espera por uma resposta, mas isso não é assíncrono. Se o servidor remoto abrir a conexão e travar, esse código aguardará 30 segundos até atingir o tempo limite.
chmac

17
o motivo pelo qual ele parece funcionar "assíncrono" porque você não lê o soquete antes de fechá-lo, para que ele não trave, mesmo que o servidor não emita uma resposta a tempo. No entanto, isso não é absolutamente assíncrono. Se o buffer de gravação estiver cheio (muito provavelmente), seu script ficará definitivamente lá. Você deve alterar o título para algo como "solicitar uma página da Web sem esperar pela resposta".
howanghk

3
Isto não é nem assíncrono nem está usando curl, como você ousa chamá-lo curl_post_asynce obter até upvotes ...
Daniel W.

27

Se você controlar o destino que deseja chamar de forma assíncrona (por exemplo, seu próprio "longtask.php"), poderá fechar a conexão a partir desse fim, e os dois scripts serão executados em paralelo. Funciona assim:

  1. quick.php abre o arquivo longtask.php via cURL (nenhuma mágica aqui)
  2. O longtask.php fecha a conexão e continua (mágica!)
  3. cURL retorna para quick.php quando a conexão é fechada
  4. Ambas as tarefas continuam em paralelo

Eu tentei isso, e funciona muito bem. Mas o quick.php não saberá nada sobre o desempenho do longtask.php, a menos que você crie algum meio de comunicação entre os processos.

Experimente esse código no longtask.php, antes de fazer qualquer outra coisa. Ele fechará a conexão, mas continuará sendo executado (e suprimirá qualquer saída):

while(ob_get_level()) ob_end_clean();
header('Connection: close');
ignore_user_abort();
ob_start();
echo('Connection Closed');
$size = ob_get_length();
header("Content-Length: $size");
ob_end_flush();
flush();

O código é copiado das notas fornecidas pelo usuário do manual do PHP e um pouco melhorado.


3
Isso funcionaria. Mas se você estiver usando uma estrutura MVC, pode ser difícil de implementar, porque a maneira como essas estruturas interceptam e reescrevem as chamadas. Por exemplo, ele não funciona em um controlador no CakePHP
Chris Cinelli

Uma dúvida sobre esse código, o processo que você precisa executar no longtask deve seguir essas linhas? Obrigado.
morgar

Não funciona perfeitamente. Tente adicionar while(true);após o seu código. A página irá travar, isso significa que ainda está sendo executada em primeiro plano.
زياد

17

Você pode fazer truques usando exec () para chamar algo que possa fazer solicitações HTTP, como wget, mas você deve direcionar toda a saída do programa para algum lugar, como um arquivo ou / dev / null, caso contrário, o processo PHP aguardará essa saída .

Se você deseja separar o processo completamente do thread do apache, tente algo como (não tenho certeza disso, mas espero que você entenda):

exec('bash -c "wget -O (url goes here) > /dev/null 2>&1 &"');

Não é um bom negócio, e você provavelmente desejará algo como um trabalho cron chamando um script de pulsação que pesquisa uma fila de eventos de banco de dados real para realizar eventos assíncronos reais.


3
Da mesma forma, também fiz o seguinte: exec ("curl $ url> / dev / null &");
Matt Huggins

2
Pergunta: existe um benefício em chamar 'bash -c "wget"' em vez de apenas 'wget'?
9789 Matt Huggins #

2
Nos meus testes, usar exec("curl $url > /dev/null 2>&1 &");é uma das soluções mais rápidas aqui. É imensamente mais rápido (1,9s para 100 iterações) do que a post_without_wait()função (14,8s) na resposta "aceita" acima. E é um one-liner ...
rinogo

Use caminho completo (por exemplo, / usr / bin / curl) para torná-lo ainda mais rápido
Putnik

isso espera até o script terminar?
cikatomo 12/04

11

A partir de 2018, o Guzzle se tornou a biblioteca padrão padrão para solicitações HTTP, usada em várias estruturas modernas. Está escrito em PHP puro e não requer a instalação de extensões personalizadas.

Ele pode fazer chamadas HTTP assíncronas muito bem e até agrupá-las , como quando você precisa fazer 100 chamadas HTTP, mas não deseja executar mais de 5 por vez.

Exemplo de solicitação simultânea

use GuzzleHttp\Client;
use GuzzleHttp\Promise;

$client = new Client(['base_uri' => 'http://httpbin.org/']);

// Initiate each request but do not block
$promises = [
    'image' => $client->getAsync('/image'),
    'png'   => $client->getAsync('/image/png'),
    'jpeg'  => $client->getAsync('/image/jpeg'),
    'webp'  => $client->getAsync('/image/webp')
];

// Wait on all of the requests to complete. Throws a ConnectException
// if any of the requests fail
$results = Promise\unwrap($promises);

// Wait for the requests to complete, even if some of them fail
$results = Promise\settle($promises)->wait();

// You can access each result using the key provided to the unwrap
// function.
echo $results['image']['value']->getHeader('Content-Length')[0]
echo $results['png']['value']->getHeader('Content-Length')[0]

Consulte http://docs.guzzlephp.org/en/stable/quickstart.html#concurrent-requests


3
No entanto, essa resposta não é assíncrona. aparentemente guzzle não faz isso
daslicious

2
Guzzle requer que você instale o curl. Caso contrário, não é paralelo e não avisa que não é paralelo.
Velizar Hristov

Obrigado pelo link @daslicious - sim, parece que não é completamente assíncrono (como quando você deseja enviar uma solicitação, mas não se importa com o resultado), mas algumas postagens nesse segmento em que um usuário ofereceu uma solução alternativa por definir um valor de tempo limite de solicitação muito baixo que ainda permita o tempo de conexão, mas não espere o resultado.
Simon East

9
/**
 * Asynchronously execute/include a PHP file. Does not record the output of the file anywhere. 
 *
 * @param string $filename              file to execute, relative to calling script
 * @param string $options               (optional) arguments to pass to file via the command line
 */ 
function asyncInclude($filename, $options = '') {
    exec("/path/to/php -f {$filename} {$options} >> /dev/null &");
}

Isso não é assíncrono porque o exec está bloqueando até que você saia ou bifurque o processo que deseja executar.
Daniel W.

6
Você notou o &no final?
philfreo

Então, isso bloquearia o script ou não, estou confuso?
pleshy

1
@pleshy não vai. comercial (&) meios para executar o script em segundo plano
daisura99

8

Você pode usar esta biblioteca: https://github.com/stil/curl-easy

É bem simples então:

<?php
$request = new cURL\Request('http://yahoo.com/');
$request->getOptions()->set(CURLOPT_RETURNTRANSFER, true);

// Specify function to be called when your request is complete
$request->addListener('complete', function (cURL\Event $event) {
    $response = $event->response;
    $httpCode = $response->getInfo(CURLINFO_HTTP_CODE);
    $html = $response->getContent();
    echo "\nDone.\n";
});

// Loop below will run as long as request is processed
$timeStart = microtime(true);
while ($request->socketPerform()) {
    printf("Running time: %dms    \r", (microtime(true) - $timeStart)*1000);
    // Here you can do anything else, while your request is in progress
}

Abaixo você pode ver a saída do console do exemplo acima. Ele exibirá um relógio ao vivo simples, indicando quanto tempo a solicitação está em execução:


animação


Esta deve ser a resposta aceite para a questão, porque, mesmo que isso não é verdade assíncrona, é melhor do que o aceito e todos os "assíncronos" respostas com guzzle (Aqui você pode executar operações enquanto a solicitação é realizada)
0ddlyoko

7
  1. Fingir um pedido de aborto usando CURLum valor baixoCURLOPT_TIMEOUT_MS

  2. definido ignore_user_abort(true)para continuar o processamento após o fechamento da conexão.

Com este método, não é necessário implementar o tratamento de conexões por meio de cabeçalhos e buffer, dependendo da versão do SO, navegador e PHP

Processo mestre

function async_curl($background_process=''){

    //-------------get curl contents----------------

    $ch = curl_init($background_process);
    curl_setopt_array($ch, array(
        CURLOPT_HEADER => 0,
        CURLOPT_RETURNTRANSFER =>true,
        CURLOPT_NOSIGNAL => 1, //to timeout immediately if the value is < 1000 ms
        CURLOPT_TIMEOUT_MS => 50, //The maximum number of mseconds to allow cURL functions to execute
        CURLOPT_VERBOSE => 1,
        CURLOPT_HEADER => 1
    ));
    $out = curl_exec($ch);

    //-------------parse curl contents----------------

    //$header_size = curl_getinfo($ch, CURLINFO_HEADER_SIZE);
    //$header = substr($out, 0, $header_size);
    //$body = substr($out, $header_size);

    curl_close($ch);

    return true;
}

async_curl('http://example.com/background_process_1.php');

Processo em segundo plano

ignore_user_abort(true);

//do something...

NB

Se você deseja que o tempo limite do cURL seja atingido em menos de um segundo, é possível usar CURLOPT_TIMEOUT_MS, embora exista um bug / "recurso" em "Sistemas Unix" que faça com que o libcurl atinja o tempo limite imediatamente se o valor for <1000 ms com o erro " cURL Erro (28): tempo limite atingido ". A explicação para esse comportamento é:

[...]

A solução é desativar os sinais usando CURLOPT_NOSIGNAL

Recursos


Como você lida com o tempo limite da conexão (resolução, DNS)? Quando eu definir timeout_ms a 1 eu sempre acabar com "resolver expirou depois de 4 ms" ou algo parecido
Martin Wickman

Eu não sei, mas 4 ms já parece muito rápido para mim ... não acho que você possa resolver mais rapidamente alterando as configurações de ondulação. Tente otimizar o pedido alvo talvez ...
RafaSashi

Ok, mas timeout_ms = 1 define o tempo limite para toda a solicitação. Portanto, se a sua resolução demorar mais de 1 ms, o tempo limite de ondulação será interrompido e a solicitação será interrompida. Não vejo como isso funcione (supondo que a resolução demore> 1 ms).
Martin Wickman

4

deixe-me mostrar o meu caminho :)

precisa do nodejs instalado no servidor

(meu servidor envia 1000 solicitações de obtenção https https leva apenas 2 segundos)

url.php:

<?
$urls = array_fill(0, 100, 'http://google.com/blank.html');

function execinbackground($cmd) { 
    if (substr(php_uname(), 0, 7) == "Windows"){ 
        pclose(popen("start /B ". $cmd, "r"));  
    } 
    else { 
        exec($cmd . " > /dev/null &");   
    } 
} 
fwite(fopen("urls.txt","w"),implode("\n",$urls);
execinbackground("nodejs urlscript.js urls.txt");
// { do your work while get requests being executed.. }
?>

urlscript.js>

var https = require('https');
var url = require('url');
var http = require('http');
var fs = require('fs');
var dosya = process.argv[2];
var logdosya = 'log.txt';
var count=0;
http.globalAgent.maxSockets = 300;
https.globalAgent.maxSockets = 300;

setTimeout(timeout,100000); // maximum execution time (in ms)

function trim(string) {
    return string.replace(/^\s*|\s*$/g, '')
}

fs.readFile(process.argv[2], 'utf8', function (err, data) {
    if (err) {
        throw err;
    }
    parcala(data);
});

function parcala(data) {
    var data = data.split("\n");
    count=''+data.length+'-'+data[1];
    data.forEach(function (d) {
        req(trim(d));
    });
    /*
    fs.unlink(dosya, function d() {
        console.log('<%s> file deleted', dosya);
    });
    */
}


function req(link) {
    var linkinfo = url.parse(link);
    if (linkinfo.protocol == 'https:') {
        var options = {
        host: linkinfo.host,
        port: 443,
        path: linkinfo.path,
        method: 'GET'
    };
https.get(options, function(res) {res.on('data', function(d) {});}).on('error', function(e) {console.error(e);});
    } else {
    var options = {
        host: linkinfo.host,
        port: 80,
        path: linkinfo.path,
        method: 'GET'
    };        
http.get(options, function(res) {res.on('data', function(d) {});}).on('error', function(e) {console.error(e);});
    }
}


process.on('exit', onExit);

function onExit() {
    log();
}

function timeout()
{
console.log("i am too far gone");process.exit();
}

function log() 
{
    var fd = fs.openSync(logdosya, 'a+');
    fs.writeSync(fd, dosya + '-'+count+'\n');
    fs.closeSync(fd);
}

1
Observe que muitos provedores de hospedagem não permitem o uso de certas funções do PHP (como popen / exec ). Consulte a diretiva disable_functions do PHP.
Eugen Mihailescu

4

A extensão swoole. https://github.com/matyhtf/swoole Estrutura de rede assíncrona e simultânea para PHP.

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);

$client->on("connect", function($cli) {
    $cli->send("hello world\n");
});

$client->on("receive", function($cli, $data){
    echo "Receive: $data\n";
});

$client->on("error", function($cli){
    echo "connect fail\n";
});

$client->on("close", function($cli){
    echo "close\n";
});

$client->connect('127.0.0.1', 9501, 0.5);

4

Você pode usar soquetes sem bloqueio e uma das extensões pecl para PHP:

Você pode usar a biblioteca que fornece uma camada de abstração entre seu código e uma extensão pecl: https://github.com/reactphp/event-loop

Você também pode usar o cliente http assíncrono, com base na biblioteca anterior: https://github.com/reactphp/http-client

Veja outras bibliotecas do ReactPHP: http://reactphp.org

Tenha cuidado com um modelo assíncrono. Eu recomendo ver este vídeo no youtube: http://www.youtube.com/watch?v=MWNcItWuKpI


3
class async_file_get_contents extends Thread{
    public $ret;
    public $url;
    public $finished;
        public function __construct($url) {
        $this->finished=false;
        $this->url=$url;
    }
        public function run() {
        $this->ret=file_get_contents($this->url);
        $this->finished=true;
    }
}
$afgc=new async_file_get_contents("http://example.org/file.ext");

2

Extensão de evento

A extensão de evento é muito apropriada. É uma porta da biblioteca Libevent, projetada para E / S orientada a eventos, principalmente para redes.

Eu escrevi um cliente HTTP de amostra que permite agendar uma série de solicitações HTTP e executá-las de forma assíncrona.

Esta é uma classe de cliente HTTP de amostra com base na extensão de Evento .

A classe permite agendar uma série de solicitações HTTP e executá-las de forma assíncrona.

http-client.php

<?php
class MyHttpClient {
  /// @var EventBase
  protected $base;
  /// @var array Instances of EventHttpConnection
  protected $connections = [];

  public function __construct() {
    $this->base = new EventBase();
  }

  /**
   * Dispatches all pending requests (events)
   *
   * @return void
   */
  public function run() {
    $this->base->dispatch();
  }

  public function __destruct() {
    // Destroy connection objects explicitly, don't wait for GC.
    // Otherwise, EventBase may be free'd earlier.
    $this->connections = null;
  }

  /**
   * @brief Adds a pending HTTP request
   *
   * @param string $address Hostname, or IP
   * @param int $port Port number
   * @param array $headers Extra HTTP headers
   * @param int $cmd A EventHttpRequest::CMD_* constant
   * @param string $resource HTTP request resource, e.g. '/page?a=b&c=d'
   *
   * @return EventHttpRequest|false
   */
  public function addRequest($address, $port, array $headers,
    $cmd = EventHttpRequest::CMD_GET, $resource = '/')
  {
    $conn = new EventHttpConnection($this->base, null, $address, $port);
    $conn->setTimeout(5);

    $req = new EventHttpRequest([$this, '_requestHandler'], $this->base);

    foreach ($headers as $k => $v) {
      $req->addHeader($k, $v, EventHttpRequest::OUTPUT_HEADER);
    }
    $req->addHeader('Host', $address, EventHttpRequest::OUTPUT_HEADER);
    $req->addHeader('Connection', 'close', EventHttpRequest::OUTPUT_HEADER);
    if ($conn->makeRequest($req, $cmd, $resource)) {
      $this->connections []= $conn;
      return $req;
    }

    return false;
  }


  /**
   * @brief Handles an HTTP request
   *
   * @param EventHttpRequest $req
   * @param mixed $unused
   *
   * @return void
   */
  public function _requestHandler($req, $unused) {
    if (is_null($req)) {
      echo "Timed out\n";
    } else {
      $response_code = $req->getResponseCode();

      if ($response_code == 0) {
        echo "Connection refused\n";
      } elseif ($response_code != 200) {
        echo "Unexpected response: $response_code\n";
      } else {
        echo "Success: $response_code\n";
        $buf = $req->getInputBuffer();
        echo "Body:\n";
        while ($s = $buf->readLine(EventBuffer::EOL_ANY)) {
          echo $s, PHP_EOL;
        }
      }
    }
  }
}


$address = "my-host.local";
$port = 80;
$headers = [ 'User-Agent' => 'My-User-Agent/1.0', ];

$client = new MyHttpClient();

// Add pending requests
for ($i = 0; $i < 10; $i++) {
  $client->addRequest($address, $port, $headers,
    EventHttpRequest::CMD_GET, '/test.php?a=' . $i);
}

// Dispatch pending requests
$client->run();

test.php

Este é um script de amostra no lado do servidor.

<?php
echo 'GET: ', var_export($_GET, true), PHP_EOL;
echo 'User-Agent: ', $_SERVER['HTTP_USER_AGENT'] ?? '(none)', PHP_EOL;

Uso

php http-client.php

Saída de amostra

Success: 200
Body:
GET: array (
  'a' => '1',
)
User-Agent: My-User-Agent/1.0
Success: 200
Body:
GET: array (
  'a' => '0',
)
User-Agent: My-User-Agent/1.0
Success: 200
Body:
GET: array (
  'a' => '3',
)
...

(Aparado.)

Observe que o código foi projetado para processamento a longo prazo na CLI SAPI .


Para protocolos personalizados, considere o uso de API de baixo nível, ou seja , eventos de buffer , buffers . Para comunicações SSL / TLS, eu recomendaria a API de baixo nível em conjunto com o contexto SSL do evento . Exemplos:


Embora a API HTTP da Libevent seja simples, ela não é tão flexível quanto os eventos do buffer. Por exemplo, a API HTTP atualmente não suporta métodos HTTP personalizados. Mas é possível implementar virtualmente qualquer protocolo usando a API de baixo nível.

Extensão Ev

Também escrevi uma amostra de outro cliente HTTP usando a extensão Ev com soquetes no modo sem bloqueio . O código é um pouco mais detalhado que o exemplo baseado em Event, porque Ev é um loop de eventos de uso geral. Ele não fornece funções específicas da rede, mas seu EvIoobservador é capaz de ouvir um descritor de arquivo encapsulado no recurso de soquete, em particular.

Este é um exemplo de cliente HTTP baseado na extensão Ev .

A extensão Ev implementa um loop de eventos de uso geral simples, porém poderoso. Ele não fornece observadores específicos da rede, mas seu observador de E / S pode ser usado para processamento assíncrono de soquetes .

O código a seguir mostra como as solicitações HTTP podem ser agendadas para processamento paralelo.

http-client.php

<?php
class MyHttpRequest {
  /// @var MyHttpClient
  private $http_client;
  /// @var string
  private $address;
  /// @var string HTTP resource such as /page?get=param
  private $resource;
  /// @var string HTTP method such as GET, POST etc.
  private $method;
  /// @var int
  private $service_port;
  /// @var resource Socket
  private $socket;
  /// @var double Connection timeout in seconds.
  private $timeout = 10.;
  /// @var int Chunk size in bytes for socket_recv()
  private $chunk_size = 20;
  /// @var EvTimer
  private $timeout_watcher;
  /// @var EvIo
  private $write_watcher;
  /// @var EvIo
  private $read_watcher;
  /// @var EvTimer
  private $conn_watcher;
  /// @var string buffer for incoming data
  private $buffer;
  /// @var array errors reported by sockets extension in non-blocking mode.
  private static $e_nonblocking = [
    11, // EAGAIN or EWOULDBLOCK
    115, // EINPROGRESS
  ];

  /**
   * @param MyHttpClient $client
   * @param string $host Hostname, e.g. google.co.uk
   * @param string $resource HTTP resource, e.g. /page?a=b&c=d
   * @param string $method HTTP method: GET, HEAD, POST, PUT etc.
   * @throws RuntimeException
   */
  public function __construct(MyHttpClient $client, $host, $resource, $method) {
    $this->http_client = $client;
    $this->host        = $host;
    $this->resource    = $resource;
    $this->method      = $method;

    // Get the port for the WWW service
    $this->service_port = getservbyname('www', 'tcp');

    // Get the IP address for the target host
    $this->address = gethostbyname($this->host);

    // Create a TCP/IP socket
    $this->socket = socket_create(AF_INET, SOCK_STREAM, SOL_TCP);
    if (!$this->socket) {
      throw new RuntimeException("socket_create() failed: reason: " .
        socket_strerror(socket_last_error()));
    }

    // Set O_NONBLOCK flag
    socket_set_nonblock($this->socket);

    $this->conn_watcher = $this->http_client->getLoop()
      ->timer(0, 0., [$this, 'connect']);
  }

  public function __destruct() {
    $this->close();
  }

  private function freeWatcher(&$w) {
    if ($w) {
      $w->stop();
      $w = null;
    }
  }

  /**
   * Deallocates all resources of the request
   */
  private function close() {
    if ($this->socket) {
      socket_close($this->socket);
      $this->socket = null;
    }

    $this->freeWatcher($this->timeout_watcher);
    $this->freeWatcher($this->read_watcher);
    $this->freeWatcher($this->write_watcher);
    $this->freeWatcher($this->conn_watcher);
  }

  /**
   * Initializes a connection on socket
   * @return bool
   */
  public function connect() {
    $loop = $this->http_client->getLoop();

    $this->timeout_watcher = $loop->timer($this->timeout, 0., [$this, '_onTimeout']);
    $this->write_watcher = $loop->io($this->socket, Ev::WRITE, [$this, '_onWritable']);

    return socket_connect($this->socket, $this->address, $this->service_port);
  }

  /**
   * Callback for timeout (EvTimer) watcher
   */
  public function _onTimeout(EvTimer $w) {
    $w->stop();
    $this->close();
  }

  /**
   * Callback which is called when the socket becomes wriable
   */
  public function _onWritable(EvIo $w) {
    $this->timeout_watcher->stop();
    $w->stop();

    $in = implode("\r\n", [
      "{$this->method} {$this->resource} HTTP/1.1",
      "Host: {$this->host}",
      'Connection: Close',
    ]) . "\r\n\r\n";

    if (!socket_write($this->socket, $in, strlen($in))) {
      trigger_error("Failed writing $in to socket", E_USER_ERROR);
      return;
    }

    $loop = $this->http_client->getLoop();
    $this->read_watcher = $loop->io($this->socket,
      Ev::READ, [$this, '_onReadable']);

    // Continue running the loop
    $loop->run();
  }

  /**
   * Callback which is called when the socket becomes readable
   */
  public function _onReadable(EvIo $w) {
    // recv() 20 bytes in non-blocking mode
    $ret = socket_recv($this->socket, $out, 20, MSG_DONTWAIT);

    if ($ret) {
      // Still have data to read. Append the read chunk to the buffer.
      $this->buffer .= $out;
    } elseif ($ret === 0) {
      // All is read
      printf("\n<<<<\n%s\n>>>>", rtrim($this->buffer));
      fflush(STDOUT);
      $w->stop();
      $this->close();
      return;
    }

    // Caught EINPROGRESS, EAGAIN, or EWOULDBLOCK
    if (in_array(socket_last_error(), static::$e_nonblocking)) {
      return;
    }

    $w->stop();
    $this->close();
  }
}

/////////////////////////////////////
class MyHttpClient {
  /// @var array Instances of MyHttpRequest
  private $requests = [];
  /// @var EvLoop
  private $loop;

  public function __construct() {
    // Each HTTP client runs its own event loop
    $this->loop = new EvLoop();
  }

  public function __destruct() {
    $this->loop->stop();
  }

  /**
   * @return EvLoop
   */
  public function getLoop() {
    return $this->loop;
  }

  /**
   * Adds a pending request
   */
  public function addRequest(MyHttpRequest $r) {
    $this->requests []= $r;
  }

  /**
   * Dispatches all pending requests
   */
  public function run() {
    $this->loop->run();
  }
}


/////////////////////////////////////
// Usage
$client = new MyHttpClient();
foreach (range(1, 10) as $i) {
  $client->addRequest(new MyHttpRequest($client, 'my-host.local', '/test.php?a=' . $i, 'GET'));
}
$client->run();

Teste

Suponha que o http://my-host.local/test.phpscript esteja imprimindo o dump de $_GET:

<?php
echo 'GET: ', var_export($_GET, true), PHP_EOL;

Em seguida, a saída do php http-client.phpcomando será semelhante à seguinte:

<<<<
HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Fri, 02 Dec 2016 12:39:54 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Powered-By: PHP/7.0.13-pl0-gentoo

1d
GET: array (
  'a' => '3',
)

0
>>>>
<<<<
HTTP/1.1 200 OK
Server: nginx/1.10.1
Date: Fri, 02 Dec 2016 12:39:54 GMT
Content-Type: text/html; charset=UTF-8
Transfer-Encoding: chunked
Connection: close
X-Powered-By: PHP/7.0.13-pl0-gentoo

1d
GET: array (
  'a' => '2',
)

0
>>>>
...

(aparado)

Note-se, em PHP 5 a tomadas de extensão pode logar avisos para EINPROGRESS, EAGAINe EWOULDBLOCK errnovalores. É possível desligar os logs com

error_reporting(E_ERROR);

Em relação ao "resto" do código

Eu só quero fazer algo como file_get_contents(), mas não espere a solicitação terminar antes de executar o restante do meu código.

O código que deve ser executado paralelamente às solicitações de rede pode ser executado no retorno de chamada de um timer de eventos , ou no observador ocioso de Ev , por exemplo. Você pode descobrir isso facilmente assistindo as amostras mencionadas acima. Caso contrário, vou adicionar outro exemplo :)


1

Aqui está um exemplo de trabalho, basta executá-lo e abrir storage.txt depois, para verificar o resultado mágico

<?php
    function curlGet($target){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $target);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec ($ch);
        curl_close ($ch);
        return $result;
    }

    // Its the next 3 lines that do the magic
    ignore_user_abort(true);
    header("Connection: close"); header("Content-Length: 0");
    echo str_repeat("s", 100000); flush();

    $i = $_GET['i'];
    if(!is_numeric($i)) $i = 1;
    if($i > 4) exit;
    if($i == 1) file_put_contents('storage.txt', '');

    file_put_contents('storage.txt', file_get_contents('storage.txt') . time() . "\n");

    sleep(5);
    curlGet($_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?i=' . ($i + 1));
    curlGet($_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?i=' . ($i + 1));

1

Aqui está minha própria função PHP quando POST para um URL específico de qualquer página .... Exemplo: *** uso da minha função ...

    <?php
        parse_str("email=myemail@ehehehahaha.com&subject=this is just a test");
        $_POST['email']=$email;
        $_POST['subject']=$subject;
        echo HTTP_POST("http://example.com/mail.php",$_POST);***

    exit;
    ?>
    <?php
    /*********HTTP POST using FSOCKOPEN **************/
    // by ArbZ

function HTTP_Post($URL,$data, $referrer="") {

    // parsing the given URL
    $URL_Info=parse_url($URL);

    // Building referrer
    if($referrer=="") // if not given use this script as referrer
        $referrer=$_SERVER["SCRIPT_URI"];

    // making string from $data
    foreach($data as $key=>$value)
        $values[]="$key=".urlencode($value);
        $data_string=implode("&",$values);

    // Find out which port is needed - if not given use standard (=80)
    if(!isset($URL_Info["port"]))
        $URL_Info["port"]=80;

    // building POST-request: HTTP_HEADERs
    $request.="POST ".$URL_Info["path"]." HTTP/1.1\n";
    $request.="Host: ".$URL_Info["host"]."\n";
    $request.="Referer: $referer\n";
    $request.="Content-type: application/x-www-form-urlencoded\n";
    $request.="Content-length: ".strlen($data_string)."\n";
    $request.="Connection: close\n";
    $request.="\n";
    $request.=$data_string."\n";

    $fp = fsockopen($URL_Info["host"],$URL_Info["port"]);
    fputs($fp, $request);
    while(!feof($fp)) {
        $result .= fgets($fp, 128);
    }
    fclose($fp); //$eco = nl2br();


    function getTextBetweenTags($string, $tagname) {
        $pattern = "/<$tagname ?.*>(.*)<\/$tagname>/";
        preg_match($pattern, $string, $matches);
        return $matches[1];
    }
    //STORE THE FETCHED CONTENTS to a VARIABLE, because its way better and fast...
    $str = $result;
    $txt = getTextBetweenTags($str, "span"); $eco = $txt;  $result = explode("&",$result);
    return $result[1];
    <span style=background-color:LightYellow;color:blue>".trim($_GET['em'])."</span>
    </pre> "; 
}
</pre>

1

Cliente http assíncrono do ReactPHP
https://github.com/shuchkin/react-http-client

Instalar via Composer

$ composer require shuchkin/react-http-client

GET HTTP assíncrono

// get.php
$loop = \React\EventLoop\Factory::create();

$http = new \Shuchkin\ReactHTTP\Client( $loop );

$http->get( 'https://tools.ietf.org/rfc/rfc2068.txt' )->then(
    function( $content ) {
        echo $content;
    },
    function ( \Exception $ex ) {
        echo 'HTTP error '.$ex->getCode().' '.$ex->getMessage();
    }
);

$loop->run();

Execute o php no modo CLI

$ php get.php

0

Acho este pacote bastante útil e muito simples: https://github.com/amphp/parallel-functions

<?php

use function Amp\ParallelFunctions\parallelMap;
use function Amp\Promise\wait;

$responses = wait(parallelMap([
    'https://google.com/',
    'https://github.com/',
    'https://stackoverflow.com/',
], function ($url) {
    return file_get_contents($url);
}));

Ele carregará todos os 3 URLs em paralelo. Você também pode usar métodos de instância de classe no fechamento.

Por exemplo, eu uso a extensão Laravel com base neste pacote https://github.com/spatie/laravel-collection-macros#parallelmap

Aqui está o meu código:

    /**
     * Get domains with all needed data
     */
    protected function getDomainsWithdata(): Collection
    {
        return $this->opensrs->getDomains()->parallelMap(function ($domain) {
            $contact = $this->opensrs->getDomainContact($domain);
            $contact['domain'] = $domain;
            return $contact;
        }, 10);
    }

Carrega todos os dados necessários em 10 threads paralelos e, em vez de 50 segundos sem assíncrono, termina em apenas 8 segundos.


0

O Symfony HttpClient é assíncrono https://symfony.com/doc/current/components/http_client.html .

Por exemplo, você pode

use Symfony\Component\HttpClient\HttpClient;

$client = HttpClient::create();
$response1 = $client->request('GET', 'https://website1');
$response2 = $client->request('GET', 'https://website1');
$response3 = $client->request('GET', 'https://website1');
//these 3 calls with return immediately
//but the requests will fire to the website1 webserver

$response1->getContent(); //this will block until content is fetched
$response2->getContent(); //same 
$response3->getContent(); //same

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.