Comment faire des requêtes HTTP asynchrones en PHP

209

Existe-t-il un moyen en PHP de faire des appels HTTP asynchrones? Je ne me soucie pas de la réponse, je veux juste faire quelque chose comme file_get_contents(), mais pas attendre la fin de la demande avant d'exécuter le reste de mon code. Ce serait super utile pour déclencher des "événements" d'une sorte dans mon application, ou déclencher de longs processus.

Des idées?

Brent
la source
9
une fonction - 'curl_multi', recherchez-la dans les documents php. Devrait résoudre vos problèmes
James Butler
22
Le titre de cet article est trompeur. Je suis venu à la recherche d' appels vraiment asynchrones similaires aux demandes dans Node.js ou à une demande AJAX. La réponse acceptée n'est pas asynchrone (elle bloque et ne fournit pas de rappel), juste une demande synchrone plus rapide. Pensez à changer la question ou la réponse acceptée.
Johntron
Jouer avec la gestion des connexions via les en-têtes et le tampon n'est pas à l'épreuve des balles. Je viens de poster une nouvelle réponse indépendante de l'OS, du navigateur ou de la version PHP
RafaSashi
1
Asynchrone ne signifie pas que vous ne vous souciez pas de la réponse. Cela signifie simplement que l'appel ne bloque pas l'exécution du thread principal. Asynchrone nécessite toujours une réponse, mais la réponse peut être traitée dans un autre thread d'exécution ou plus tard dans une boucle d'événement. Cette question demande une demande de tir et d'oubli qui peut être synchrone ou asynchrone selon la sémantique de remise des messages, que vous vous souciez de l'ordre des messages ou de la confirmation de remise.
CMCDragonkai
Je pense que vous devriez faire cette requête HTTP de feu en mode non bloquant (w / c est ce que vous voulez vraiment). Parce que lorsque vous appelez une ressource, vous voulez essentiellement savoir si vous avez atteint le serveur ou non (ou pour une raison quelconque, vous avez simplement besoin de la réponse). La meilleure réponse est vraiment fsockopen et mettre la lecture ou l'écriture de flux en mode non bloquant. C'est comme appeler et oublier.
KiX Ortillan

Réponses:

42

La réponse que j'avais précédemment acceptée ne fonctionnait pas. Il attendait toujours les réponses. Cela fonctionne cependant, tiré de Comment puis-je faire une demande GET asynchrone 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
la source
67
Ce n'est PAS asynchrone! En particulier, si le serveur de l'autre côté est en panne, ce morceau de code se bloque pendant 30 secondes (le 5ème paramètre dans fsockopen). De plus, le fwrite va prendre son temps doux à exécuter (que vous pouvez limiter avec stream_set_timeout ($ fp, $ my_timeout). Le mieux que vous puissiez faire est de définir un délai d'attente bas sur fsockopen à 0,1 (100 ms) et $ my_timeout à 100 ms Vous risquez cependant que le délai d'expiration de la demande.
Chris Cinelli
3
Je vous assure que c'est asynchrone et ne prend pas 30 secondes. C'est un timeout max. Il est possible que vos paramètres soient différents, ce qui cause cet effet, mais cela a très bien fonctionné pour moi.
Brent
11
@UltimateBrent Il n'y a rien dans le code qui suggère qu'il est asynchrone. Il n'attend pas de réponse, mais ce n'est pas asynchrone. Si le serveur distant ouvre la connexion puis se bloque, ce code attendra 30 secondes jusqu'à ce que vous atteigniez ce délai.
chmac
17
la raison pour laquelle il semble fonctionner "async" parce que vous ne lisez pas depuis le socket avant de le fermer donc il ne se bloque pas même si le serveur n'a pas émis de réponse à temps. Cependant, ce n'est absolument pas asynchrone. Si le tampon d'écriture est plein (très probablement), votre script s'y bloquera définitivement. Vous devriez envisager de changer votre titre en quelque chose comme «demander une page Web sans attendre de réponse».
howanghk
3
Ce n'est ni asynchrone ni utiliser curl, comment vous osez l'appeler curl_post_asyncet obtenir même des votes positifs ...
Daniel W.
27

Si vous contrôlez la cible que vous souhaitez appeler de manière asynchrone (par exemple, votre propre "longtask.php"), vous pouvez fermer la connexion à cette fin et les deux scripts s'exécuteront en parallèle. Cela fonctionne comme ceci:

  1. quick.php ouvre longtask.php via cURL (pas de magie ici)
  2. longtask.php ferme la connexion et continue (magique!)
  3. cURL revient à quick.php lorsque la connexion est fermée
  4. Les deux tâches se poursuivent en parallèle

J'ai essayé cela, et cela fonctionne très bien. Mais quick.php ne saura rien sur la durée de longtask.php, sauf si vous créez des moyens de communication entre les processus.

Essayez ce code dans longtask.php, avant de faire quoi que ce soit d'autre. Il fermera la connexion, mais continuera à fonctionner (et supprimera toute sortie):

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

Le code est copié à partir des notes fournies par l'utilisateur du manuel PHP et quelque peu amélioré.

Christian Davén
la source
3
Cela fonctionnerait. Mais si vous utilisez un framework MVC, il peut être difficile à implémenter en raison de la façon dont ces framework interceptent et réécrivent les appels. Par exemple, cela ne fonctionne pas dans un contrôleur dans CakePHP
Chris Cinelli
Un doute sur ce code, le processus que vous devez faire dans longtask doit aller après ces lignes? Merci.
morgar
Cela ne fonctionne pas parfaitement. Essayez d'ajouter while(true);après votre code. La page va se bloquer, cela signifie qu'elle est toujours en cours d'exécution au premier plan.
زياد
17

Vous pouvez faire de la ruse en utilisant exec () pour appeler quelque chose qui peut faire des requêtes HTTP, comme wget, mais vous devez diriger toutes les sorties du programme vers quelque part, comme un fichier ou / dev / null, sinon le processus PHP attendra cette sortie .

Si vous voulez séparer complètement le processus du thread apache, essayez quelque chose comme (je ne suis pas sûr de cela, mais j'espère que vous avez l'idée):

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

Ce n'est pas une bonne affaire, et vous voudrez probablement quelque chose comme un travail cron invoquant un script de pulsation qui interroge une file d'attente d'événements de base de données réelle pour faire de vrais événements asynchrones.

Ami Internet
la source
3
De même, j'ai également fait ce qui suit: exec ("curl $ url> / dev / null &");
Matt Huggins,
2
Question: y a-t-il un avantage à appeler 'bash -c "wget"' plutôt que simplement 'wget'?
Matt Huggins
2
Dans mes tests, l'utilisation exec("curl $url > /dev/null 2>&1 &");est l'une des solutions les plus rapides ici. C'est immensément plus rapide (1,9 s pour 100 itérations) que la post_without_wait()fonction (14,8 s) dans la réponse «acceptée» ci-dessus. ET c'est un
aller simple
Utilisez le chemin complet (par exemple / usr / bin / curl) pour le rendre encore plus rapide
Putnik
cela attend-il que le script soit terminé?
cikatomo
12

Depuis 2018, Guzzle est devenu la bibliothèque standard de facto pour les requêtes HTTP, utilisée dans plusieurs frameworks modernes. Il est écrit en PHP pur et ne nécessite aucune installation d'extensions personnalisées.

Il peut très bien faire des appels HTTP asynchrones et même les regrouper, par exemple lorsque vous devez effectuer 100 appels HTTP, mais que vous ne souhaitez pas exécuter plus de 5 appels à la fois.

Exemple de demande simultanée

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]

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

Simon East
la source
3
Cependant, cette réponse n'est pas asynchrone. apparemment, Guzzle ne fait pas ça
daslicious
2
Guzzle vous oblige à installer curl. Sinon, il n'est pas parallèle et il ne vous avertit pas qu'il n'est pas parallèle.
Velizar Hristov
Merci pour le lien @daslicious - oui, il semble que ce ne soit pas complètement asynchrone (comme lorsque vous voulez envoyer une demande mais ne vous souciez pas du résultat) mais quelques articles dans ce fil de discussion par lequel un utilisateur a proposé une solution de contournement définir une valeur de délai d'expiration de requête très faible qui autorise toujours le temps de connexion, mais n'attend pas le résultat.
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
la source
Ce n'est pas asynchrone car exec bloque jusqu'à ce que vous quittiez ou forkiez le processus que vous souhaitez exécuter.
Daniel W.
6
Avez-vous remarqué le &à la fin?
philfreo
Est-ce que cela bloquerait alors le script ou non, je suis confus?
pleshy
1
@pleshy ce ne sera pas le cas. esperluette (&) signifie exécuter le script en arrière
daisura99
8

Vous pouvez utiliser cette bibliothèque: https://github.com/stil/curl-easy

C'est assez simple alors:

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

Ci-dessous, vous pouvez voir la sortie de la console de l'exemple ci-dessus. Il affichera une horloge en direct simple indiquant combien de temps la demande est en cours d'exécution:


animation

toujours
la source
Cela devrait être la réponse acceptée à la question car, même si ce n'est pas vrai async, c'est mieux que la réponse acceptée et toutes les réponses "async" avec guzzle (ici, vous pouvez effectuer des opérations pendant que la demande est effectuée)
0ddlyoko
7
  1. Faux une demande d'avortement en utilisant CURLun réglage basCURLOPT_TIMEOUT_MS

  2. configuré ignore_user_abort(true)pour continuer le traitement après la fermeture de la connexion.

Avec cette méthode, pas besoin d'implémenter la gestion des connexions via des en-têtes et un tampon trop dépendants de la version OS, Browser et PHP

Processus maître

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

Processus d'arrière-plan

ignore_user_abort(true);

//do something...

NB

Si vous voulez que cURL expire en moins d'une seconde, vous pouvez utiliser CURLOPT_TIMEOUT_MS, bien qu'il y ait un bogue / "fonctionnalité" sur "les systèmes de type Unix" qui fait que libcurl expire immédiatement si la valeur est <1000 ms avec l'erreur " Erreur cURL (28): le délai a été atteint ". L'explication de ce comportement est:

[...]

La solution consiste à désactiver les signaux à l'aide de CURLOPT_NOSIGNAL

Ressources

RafaSashi
la source
Comment gérez-vous le délai de connexion (résolution, DNS)? Lorsque je mets timeout_ms à 1, je me retrouve toujours avec "la résolution du délai après 4 ms" ou quelque chose comme ça
Martin Wickman
Je ne sais pas mais 4 ms sonnent déjà assez rapidement pour moi ... Je ne pense pas que vous puissiez résoudre plus rapidement en modifiant les paramètres de boucle. Essayez peut-être d'optimiser la demande ciblée ...
RafaSashi
D'accord, mais timeout_ms = 1 définit le délai d'expiration pour toute la demande. Donc, si votre résolution prend plus de 1 ms, la boucle expirera et arrêtera la demande. Je ne vois pas du tout comment cela peut fonctionner (en supposant que la résolution prenne> 1 ms).
Martin Wickman
4

permettez-moi de vous montrer mon chemin :)

nécessite nodejs installé sur le serveur

(mon serveur envoie 1000 https pour obtenir une requête en 2 secondes seulement)

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);
}
user1031143
la source
1
Veuillez noter que de nombreux hébergeurs n'autorisent pas l'utilisation de certaines fonctions PHP (comme popen / exec ). Voir directive PHP disable_functions.
Eugen Mihailescu
4

L'extension Swoole. https://github.com/matyhtf/swoole Framework de réseau asynchrone et simultané pour 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
la source
4

Vous pouvez utiliser des sockets non bloquants et l'une des extensions pecl pour PHP:

Vous pouvez utiliser une bibliothèque qui vous donne une couche d'abstraction entre votre code et une extension pecl: https://github.com/reactphp/event-loop

Vous pouvez également utiliser le client http asynchrone, basé sur la bibliothèque précédente: https://github.com/reactphp/http-client

Voir d'autres bibliothèques de ReactPHP: http://reactphp.org

Soyez prudent avec un modèle asynchrone. Je recommande de voir cette vidéo sur youtube: http://www.youtube.com/watch?v=MWNcItWuKpI

Roman Shamritskiy
la source
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
la source
2

Extension d'événement

L' extension d' événement est très appropriée. Il s'agit d'un port de la bibliothèque Libevent conçu pour les E / S événementielles, principalement pour la mise en réseau.

J'ai écrit un exemple de client HTTP qui permet de planifier un certain nombre de requêtes HTTP et de les exécuter de manière asynchrone.

Il s'agit d'un exemple de classe client HTTP basée sur l' extension d' événement .

La classe permet de planifier un certain nombre de requêtes HTTP, puis de les exécuter de manière asynchrone.

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

Il s'agit d'un exemple de script côté serveur.

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

Usage

php http-client.php

Exemple de sortie

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

(Découpé.)

Remarque, le code est conçu pour un traitement à long terme dans l' interface CLI SAPI .


Pour les protocoles personnalisés, envisagez d'utiliser une API de bas niveau, c'est-à-dire des événements de tampon , des tampons . Pour les communications SSL / TLS, je recommanderais l'API de bas niveau en conjonction avec le contexte SSL de l'événement . Exemples:


Bien que l'API HTTP de Libevent soit simple, elle n'est pas aussi flexible que les événements de tampon. Par exemple, l'API HTTP ne prend actuellement pas en charge les méthodes HTTP personnalisées. Mais il est possible d'implémenter pratiquement n'importe quel protocole à l'aide de l'API de bas niveau.

Extension Ev

J'ai également écrit un échantillon d'un autre client HTTP utilisant l' extension Ev avec des sockets en mode non bloquant . Le code est légèrement plus détaillé que l'exemple basé sur Event, car Ev est une boucle d'événement à usage général. Il ne fournit pas de fonctions spécifiques au réseau, mais son EvIoobservateur est capable d'écouter un descripteur de fichier encapsulé dans la ressource socket, en particulier.

Il s'agit d'un exemple de client HTTP basé sur l' extension Ev .

L'extension Ev implémente une boucle d'événements à usage général simple mais puissante. Il ne fournit pas d'observateurs spécifiques au réseau, mais son observateur d'E / S peut être utilisé pour le traitement asynchrone des sockets .

Le code suivant montre comment les requêtes HTTP peuvent être planifiées pour un traitement parallèle.

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

Essai

Supposons que le http://my-host.local/test.phpscript imprime le vidage de $_GET:

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

La sortie de la php http-client.phpcommande sera alors similaire à la suivante:

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

(coupé)

Remarque, en PHP 5 , la prises extension peut connecter des avertissements pour EINPROGRESS, EAGAINet les EWOULDBLOCK errnovaleurs. Il est possible de désactiver les journaux avec

error_reporting(E_ERROR);

Concernant "le reste" du code

Je veux juste faire quelque chose comme file_get_contents(), mais pas attendre la fin de la requête avant d'exécuter le reste de mon code.

Le code qui est censé s'exécuter en parallèle avec les requêtes réseau peut être exécuté dans le rappel d'un temporisateur d'événement , ou l' observateur inactif d'Ev , par exemple. Vous pouvez facilement le découvrir en regardant les échantillons mentionnés ci-dessus. Sinon, je vais ajouter un autre exemple :)

Ruslan Osmanov
la source
1

Voici un exemple de travail, lancez-le et ouvrez ensuite storage.txt, pour vérifier le résultat magique

<?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
la source
1

Voici ma propre fonction PHP lorsque je fais du POST vers une URL spécifique de n'importe quelle page .... Exemple: *** utilisation de ma fonction ...

    <?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>
je suis ArbZ
la source
1

ReactPHP async http client
https://github.com/shuchkin/react-http-client

Installer via Composer

$ composer require shuchkin/react-http-client

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

Exécutez php en mode CLI

$ php get.php
Sergey Shuchkin
la source
0

Je trouve ce package assez utile et très 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);
}));

Il chargera les 3 URL en parallèle. Vous pouvez également utiliser des méthodes d'instance de classe dans la fermeture.

Par exemple, j'utilise l'extension Laravel basée sur ce package https://github.com/spatie/laravel-collection-macros#parallelmap

Voici mon code:

    /**
     * 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);
    }

Il charge toutes les données nécessaires dans 10 threads parallèles et au lieu de 50 secondes sans asynchronisation, il s'est terminé en seulement 8 secondes.

Vedmant
la source
0

Symfony HttpClient est asynchrone https://symfony.com/doc/current/components/http_client.html .

Par exemple, vous pouvez

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
nacholibre
la source
-4

Eh bien, le délai peut être défini en millisecondes, voir "CURLOPT_CONNECTTIMEOUT_MS" dans http://www.php.net/manual/en/function.curl-setopt

Akhil Sikri
la source
3
Il a seulement mis un plafond pensé un temps mort. Ce n'est pas du tout asynchrone.
Chris Cinelli