Cómo hacer solicitudes HTTP asincrónicas en PHP

209

¿Hay alguna manera en PHP de hacer llamadas HTTP asincrónicas? No me importa la respuesta, solo quiero hacer algo como file_get_contents(), pero no esperar a que finalice la solicitud antes de ejecutar el resto de mi código. Esto sería muy útil para activar "eventos" de un tipo en mi aplicación o desencadenar procesos largos.

¿Algunas ideas?

Brent
fuente
9
una función: 'curl_multi', búsquela en los documentos php. Debería resolver sus problemas
James Butler
22
El título de esta publicación es engañoso. Vine en busca de llamadas verdaderamente asincrónicas similares a las solicitudes en Node.js o una solicitud AJAX. La respuesta aceptada no es asíncrona (bloquea y no proporciona una devolución de llamada), solo una solicitud síncrona más rápida. Considere cambiar la pregunta o la respuesta aceptada.
Johntron
Jugar con el manejo de conexiones a través de encabezados y búfer no es a prueba de balas. Acabo de publicar una nueva respuesta independiente del sistema operativo, navegador o versión de PHP
RafaSashi
1
Asincrónico no significa que no te importe la respuesta. Solo significa que la llamada no bloquea la ejecución del hilo principal. Asíncrono todavía requiere una respuesta, pero la respuesta puede procesarse en otro hilo de ejecución o más tarde en un bucle de eventos. Esta pregunta es una solicitud de disparar y olvidar que puede ser síncrona o asíncrona dependiendo de la semántica de entrega del mensaje, ya sea que le interese el pedido del mensaje o la confirmación de entrega.
CMCDragonkai
Creo que deberías hacer esta solicitud HTTP de disparo en modo sin bloqueo (w / c es lo que realmente quieres). Porque cuando llamas a un recurso, básicamente quieres saber si llegaste al servidor o no (o por alguna razón, simplemente necesitas la respuesta). La mejor respuesta es fsockopen y configurar la lectura o escritura de secuencias en modo sin bloqueo. Es como llamar y olvidar.
KiX Ortillan

Respuestas:

42

La respuesta que había aceptado anteriormente no funcionó. Todavía esperaba respuestas. Sin embargo, esto funciona, tomado de ¿Cómo hago una solicitud GET asincrónica en 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);
}
Brent
fuente
67
¡Esto NO es asíncrono! En particular, si el servidor del otro lado está inactivo, este código se bloqueará durante 30 segundos (el quinto parámetro en fsockopen). Además, fwrite se tomará su dulce tiempo de ejecución (que puede limitar con stream_set_timeout ($ fp, $ my_timeout). Lo mejor que puede hacer es establecer un tiempo de espera bajo en fsockopen a 0.1 (100ms) y $ my_timeout a 100ms Sin embargo, corre el riesgo de que la solicitud se agote.
Chris Cinelli
3
Le aseguro que es asíncrono y que no toma 30 segundos. Eso es un tiempo de espera máximo. Es factible que sus configuraciones sean diferentes causando ese efecto, pero esto funcionó muy bien para mí.
Brent
11
@UltimateBrent No hay nada en el código que sugiera que sea asíncrono. No espera una respuesta, pero eso no es asíncrono. Si el servidor remoto abre la conexión y luego se cuelga, este código esperará 30 segundos hasta que llegue ese tiempo de espera.
chmac
17
la razón por la que parece funcionar "asíncrono" porque no lee desde el socket antes de cerrarlo, por lo que no se bloqueó incluso si el servidor no emitió una respuesta a tiempo. Sin embargo, esto no es absolutamente asíncrono. Si el búfer de escritura está lleno (lo menos probable), su script definitivamente se quedará allí. Debería considerar cambiar su título a algo así como "solicitar una página web sin esperar respuesta".
howanghk
3
Esto no es asíncrono ni está usando curl, cómo te atreves a llamarlo curl_post_asyncy obtener incluso votos a favor ...
Daniel W.
27

Si controla el destino al que desea llamar de forma asíncrona (por ejemplo, su propio "longtask.php"), puede cerrar la conexión desde ese extremo y ambos scripts se ejecutarán en paralelo. Funciona así:

  1. quick.php abre longtask.php a través de cURL (no hay magia aquí)
  2. longtask.php cierra la conexión y continúa (¡magia!)
  3. cURL vuelve a quick.php cuando se cierra la conexión
  4. Ambas tareas continúan en paralelo.

He intentado esto y funciona bien. Pero quick.php no sabrá nada sobre el funcionamiento de longtask.php, a menos que cree algún medio de comunicación entre los procesos.

Pruebe este código en longtask.php, antes de hacer cualquier otra cosa. Cerrará la conexión, pero seguirá ejecutándose (y suprimirá cualquier salida):

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();

El código se copia de las notas aportadas por el usuario del manual de PHP y se mejora un poco.

Christian Davén
fuente
3
Esto funcionaria. Pero si está utilizando un marco MVC, puede ser difícil de implementar debido a la forma en que este marco intercepta y reescribe las llamadas. Por ejemplo, no funciona en un controlador en CakePHP
Chris Cinelli
Una duda sobre este código, ¿el proceso que debe realizar en la tarea larga debe ir después de estas líneas? Gracias.
morgar
No funciona a la perfección. Intenta agregar while(true);después de tu código. La página se bloqueará, esto significa que todavía se está ejecutando en primer plano.
زياد
17

Puede hacer trucos usando exec () para invocar algo que puede hacer solicitudes HTTP, como wget, pero debe dirigir toda la salida del programa a algún lugar, como un archivo o / dev / null, de lo contrario el proceso PHP esperará esa salida .

Si desea separar el proceso del hilo de Apache por completo, intente algo como (no estoy seguro de esto, pero espero que tenga la idea):

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

No es un buen negocio, y es probable que desee algo como un trabajo cron que invoca un script de latido que sondea una cola de eventos de base de datos real para hacer eventos asincrónicos reales.

Amigo de internet
fuente
3
Del mismo modo, también he hecho lo siguiente: exec ("curl $ url> / dev / null &");
Matt Huggins
2
Pregunta: ¿hay un beneficio de llamar 'bash -c "wget"' en lugar de solo 'wget'?
Matt Huggins
2
En mis pruebas, usar exec("curl $url > /dev/null 2>&1 &");es una de las soluciones más rápidas aquí. Es inmensamente más rápido (1.9s para 100 iteraciones) que la post_without_wait()función (14.8s) en la respuesta "aceptada" anterior. Y es una
frase
Use la ruta completa (p. Ej. / Usr / bin / curl) para hacerlo aún más rápido
Putnik
¿Espera esto hasta que finalice el script?
cikatomo
11

A partir de 2018, Guzzle ha convertido en la biblioteca estándar de facto para solicitudes HTTP, utilizada en varios marcos modernos. Está escrito en PHP puro y no requiere la instalación de extensiones personalizadas.

Puede hacer llamadas HTTP asincrónicas muy bien e incluso agruparlas como cuando necesita hacer 100 llamadas HTTP, pero no desea ejecutar más de 5 a la vez.

Ejemplo de solicitud concurrente

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]

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

Simon East
fuente
3
Sin embargo, esta respuesta no es asincrónica. Aparentemente, Guzzle no hace eso
Daslicious
2
Guzzle requiere que instales el rizo. De lo contrario, no es paralelo y no le da ninguna advertencia de que no es paralelo.
Velizar Hristov
Gracias por el enlace @daslicious: sí, parece que no es completamente asíncrono (como cuando quieres enviar una solicitud pero no te importa el resultado), pero unas pocas publicaciones en ese hilo por las que un usuario ha ofrecido una solución alternativa establecer un valor de tiempo de espera de solicitud muy bajo que todavía permite el tiempo de conexión, pero no espera el 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 &");
}
philfreo
fuente
Esto no es asíncrono porque el ejecutivo está bloqueando hasta que salga o bifurque el proceso que desea ejecutar.
Daniel W.
66
¿Notaste el &al final?
philfreo
Entonces, ¿bloquearía el guión entonces o no, estoy confundido?
carnoso
1
@pleshy no lo hará. ampersand (&) significa ejecutar el script en segundo plano
daisura99
8

Puedes usar esta biblioteca: https://github.com/stil/curl-easy

Es bastante sencillo entonces:

<?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
}

A continuación puede ver la salida de la consola del ejemplo anterior. Mostrará un reloj en vivo simple que indica cuánto tiempo se está ejecutando la solicitud:


animación

stil
fuente
Esta debería ser la respuesta aceptada a la pregunta porque, incluso si no es verdadera asincrónica, es mejor que la respuesta aceptada y todas las respuestas "asincrónicas" con guzzle (Aquí puede realizar operaciones mientras se realiza la solicitud)
0ddlyoko
7
  1. Falsifica una solicitud de aborto usando CURLuna configuración bajaCURLOPT_TIMEOUT_MS

  2. configurado ignore_user_abort(true)para seguir procesando después de cerrar la conexión.

Con este método, no es necesario implementar el manejo de la conexión a través de encabezados y búfer demasiado dependientes de la versión del sistema operativo, navegador y PHP

Proceso maestro

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');

Proceso de fondo

ignore_user_abort(true);

//do something...

nótese bien

Si desea que cURL agote el tiempo de espera en menos de un segundo, puede usar CURLOPT_TIMEOUT_MS, aunque hay un error / "característica" en "sistemas tipo Unix" que hace que libcurl agote el tiempo de espera inmediatamente si el valor es <1000 ms con el error " Error de cURL (28): Se alcanzó el tiempo de espera ". La explicación de este comportamiento es:

[...]

La solución es deshabilitar las señales usando CURLOPT_NOSIGNAL

Recursos

RafaSashi
fuente
¿Cómo manejas el tiempo de espera de conexión (resolver, dns)? Cuando configuro timeout_ms en 1, siempre termino con "resolver el tiempo de espera agotado después de 4 ms" o algo así
Martin Wickman
No lo sé, pero 4 ms ya me suena bastante rápido ... No creo que puedas resolverlo más rápido cambiando cualquier configuración de rizo. Intente optimizar la solicitud dirigida quizás ...
RafaSashi
Ok, pero timeout_ms = 1 establece el tiempo de espera para toda la solicitud. Por lo tanto, si su resolución demora más de 1 ms, curl expirará y detendrá la solicitud. No veo cómo esto puede funcionar en absoluto (suponiendo que la resolución tome> 1 ms).
Martin Wickman el
4

déjame mostrarte mi camino :)

necesita nodejs instalado en el servidor

(mi servidor envía 1000 solicitudes de obtención de https solo toma 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);
}
usuario1031143
fuente
1
Tenga en cuenta que muchos proveedores de alojamiento no permiten el uso de ciertas funciones de PHP (como popen / exec ). Ver la directiva PHP disable_functions.
Eugen Mihailescu
4

La extensión del swoole. https://github.com/matyhtf/swoole Marco de red asincrónico y concurrente 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);
Tony
fuente
4

Puede usar sockets sin bloqueo y una de las extensiones pecl para PHP:

Puede usar la biblioteca que le brinda una capa de abstracción entre su código y una extensión pecl: https://github.com/reactphp/event-loop

También puede usar un cliente http asíncrono, basado en la biblioteca anterior: https://github.com/reactphp/http-client

Ver otras bibliotecas de ReactPHP: http://reactphp.org

Tenga cuidado con un modelo asincrónico. Recomiendo ver este video en youtube: http://www.youtube.com/watch?v=MWNcItWuKpI

Roman Shamritskiy
fuente
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");
Hanshenrik
fuente
2

Extensión de evento

La extensión del evento es muy apropiada. Es un puerto de la biblioteca Libevent que está diseñado para E / S controladas por eventos, principalmente para redes.

He escrito un cliente HTTP de muestra que permite programar una serie de solicitudes HTTP y ejecutarlas de forma asincrónica.

Esta es una clase de cliente HTTP de muestra basada en la extensión de evento .

La clase permite programar una serie de solicitudes HTTP y luego ejecutarlas de forma asincrónica.

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();

prueba.php

Este es un script de muestra en el lado del 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

Salida de muestra

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',
)
...

(Recortado)

Tenga en cuenta que el código está diseñado para el procesamiento a largo plazo en el CLI SAPI .


Para protocolos personalizados, considere usar API de bajo nivel, es decir , eventos de búfer , búferes . Para las comunicaciones SSL / TLS, recomendaría la API de bajo nivel junto con el contexto ssl de Event . Ejemplos:


Aunque la API HTTP de Libevent es simple, no es tan flexible como los eventos de búfer. Por ejemplo, la API HTTP actualmente no admite métodos HTTP personalizados. Pero es posible implementar prácticamente cualquier protocolo utilizando la API de bajo nivel.

Ev extensión

También he escrito una muestra de otro cliente HTTP que usa la extensión Ev con sockets en modo sin bloqueo . El código es un poco más detallado que el ejemplo basado en Evento, porque Ev es un bucle de evento de propósito general. No proporciona funciones específicas de la red, pero su EvIoobservador es capaz de escuchar un descriptor de archivo encapsulado en el recurso de socket, en particular.

Este es un cliente HTTP de muestra basado en la extensión Ev .

La extensión Ev implementa un bucle de evento de propósito general simple pero potente. No proporciona observadores específicos de la red, pero su observador de E / S se puede usar para el procesamiento asíncrono de sockets .

El siguiente código muestra cómo se pueden programar las solicitudes HTTP para el procesamiento 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();

Pruebas

Supongamos que el http://my-host.local/test.phpscript está imprimiendo el volcado de $_GET:

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

Entonces la salida del php http-client.phpcomando será similar a la siguiente:

<<<<
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
>>>>
...

(recortado)

Tenga en cuenta, en PHP 5, la tomas de extensión puede registrar advertencias para EINPROGRESS, EAGAINy EWOULDBLOCK errnovalores. Es posible apagar los registros con

error_reporting(E_ERROR);

Sobre "el resto" del Código

Solo quiero hacer algo como file_get_contents(), pero no esperar a que finalice la solicitud antes de ejecutar el resto de mi código.

El código que se supone que debe ejecutarse en paralelo con las solicitudes de red puede ejecutarse dentro de la devolución de llamada de un temporizador de eventos , o el observador inactivo de Ev , por ejemplo. Puede resolverlo fácilmente observando las muestras mencionadas anteriormente. De lo contrario, agregaré otro ejemplo :)

Ruslan Osmanov
fuente
1

Aquí hay un ejemplo de trabajo, simplemente ejecútelo y abra storage.txt luego, para verificar el 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));
AlexTR
fuente
1

Aquí está mi propia función PHP cuando hago POST a una URL específica de cualquier página ... Ejemplo: *** uso de mi función ...

    <?php
        parse_str("[email protected]&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>
soy ArbZ
fuente
1

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

Instalar a través de Composer

$ composer require shuchkin/react-http-client

HTTP asíncrono GET

// 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();

Ejecute php en modo CLI

$ php get.php
Sergey Shuchkin
fuente
0

Este paquete me parece bastante útil y muy simple: 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);
}));

Cargará las 3 URL en paralelo. También puede usar métodos de instancia de clase en el cierre.

Por ejemplo, uso la extensión Laravel basada en este paquete https://github.com/spatie/laravel-collection-macros#parallelmap

Aquí está mi 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);
    }

Carga todos los datos necesarios en 10 subprocesos paralelos y, en lugar de 50 segundos sin asíncrono, terminó en solo 8 segundos.

Vedmant
fuente
0

Symfony HttpClient es asíncrono https://symfony.com/doc/current/components/http_client.html .

Por ejemplo puedes

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
Nacho Libre
fuente