So stellen Sie asynchrone HTTP-Anforderungen in PHP

209

Gibt es in PHP eine Möglichkeit, asynchrone HTTP-Aufrufe durchzuführen? Die Antwort ist mir egal, ich möchte nur so etwas tun file_get_contents(), aber nicht warten, bis die Anforderung abgeschlossen ist, bevor ich den Rest meines Codes ausführe. Dies wäre sehr nützlich, um "Ereignisse" einer Art in meiner Anwendung auszulösen oder lange Prozesse auszulösen.

Irgendwelche Ideen?

Brent
quelle
9
eine Funktion - 'curl_multi', suchen Sie in den PHP-Dokumenten danach. Sollte Ihre Probleme lösen
James Butler
22
Der Titel dieses Beitrags ist irreführend. Ich suchte nach wirklich asynchronen Aufrufen, die Anforderungen in Node.js oder einer AJAX-Anforderung ähneln. Die akzeptierte Antwort ist nicht asynchron (sie blockiert und bietet keinen Rückruf), sondern nur eine schnellere synchrone Anfrage. Überlegen Sie, ob Sie die Frage oder die akzeptierte Antwort ändern möchten.
Johntron
Das Spielen mit der Verbindungsbehandlung über Header und Puffer ist nicht kugelsicher. Ich habe gerade eine neue Antwort unabhängig von Betriebssystem, Browser oder PHP-Version veröffentlicht
RafaSashi
1
Asynchron bedeutet nicht, dass Ihnen die Antwort egal ist. Es bedeutet nur, dass der Aufruf die Ausführung des Hauptthreads nicht blockiert. Asynchron erfordert weiterhin eine Antwort, die Antwort kann jedoch in einem anderen Ausführungsthread oder später in einer Ereignisschleife verarbeitet werden. Bei dieser Frage wird nach einer Feuer-und-Vergessen-Anforderung gefragt, die abhängig von der Semantik der Nachrichtenübermittlung, unabhängig davon, ob Sie sich für die Nachrichtenreihenfolge oder die Zustellbestätigung interessieren, synchron oder asynchron sein kann.
CMCDragonkai
Ich denke, Sie sollten diese Feuer-HTTP-Anfrage im nicht blockierenden Modus stellen (w / c ist das, was Sie wirklich wollen). Denn wenn Sie eine Ressource aufrufen, möchten Sie im Grunde wissen, ob Sie den Server erreicht haben oder nicht (oder aus welchem ​​Grund auch immer). Sie brauchen einfach die Antwort). Die beste Antwort ist wirklich fsockopen und das Lesen oder Schreiben von Streams in den nicht blockierenden Modus zu versetzen. Es ist wie anrufen und vergessen.
KiX Ortillan

Antworten:

42

Die Antwort, die ich zuvor akzeptiert hatte, funktionierte nicht. Es wartete immer noch auf Antworten. Dies funktioniert jedoch, entnommen aus Wie mache ich eine asynchrone GET-Anfrage in 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
quelle
67
Dies ist NICHT asynchron! Insbesondere wenn der Server auf der anderen Seite ausgefallen ist, bleibt dieser Code 30 Sekunden lang hängen (der 5. Parameter im fsockopen). Außerdem wird die Ausführung des fwrite seine süße Zeit in Anspruch nehmen (die Sie mit stream_set_timeout ($ fp, $ my_timeout) begrenzen können. Das Beste, was Sie tun können, ist, ein niedriges Timeout für fsockopen auf 0,1 (100 ms) und $ my_timeout auf 100 ms festzulegen Sie riskieren jedoch, dass die Anfrage eine Zeitüberschreitung aufweist.
Chris Cinelli
3
Ich versichere Ihnen, dass es asynchron ist und keine 30 Sekunden dauert. Das ist eine Zeitüberschreitung von max. Es ist möglich, dass Ihre Einstellungen unterschiedlich sind, was diesen Effekt verursacht, aber das hat bei mir großartig funktioniert.
Brent
11
@UltimateBrent Der Code enthält nichts, was darauf hindeutet, dass er asynchron ist. Es wartet nicht auf eine Antwort, aber das ist nicht asynchron. Wenn der Remote-Server die Verbindung öffnet und dann hängt, wartet dieser Code 30 Sekunden, bis Sie dieses Zeitlimit erreichen.
Chmac
17
Der Grund dafür ist, dass es "asynchron" zu funktionieren scheint, weil Sie vor dem Schließen nicht aus dem Socket lesen, sodass es nicht hängen bleibt, auch wenn der Server nicht rechtzeitig eine Antwort ausgegeben hat. Dies ist jedoch absolut nicht asynchron. Wenn der Schreibpuffer voll ist (sehr unwahrscheinlich), hängt Ihr Skript definitiv dort. Sie sollten in Betracht ziehen, Ihren Titel in "Anfordern einer Webseite ohne Warten auf eine Antwort" zu ändern.
Howanghk
3
Dies ist weder asynchron noch verwendet es Curl, wie Sie es wagen, es zu nennen curl_post_asyncund sogar positive Stimmen zu erhalten ...
Daniel W.
27

Wenn Sie das Ziel steuern, das Sie asynchron aufrufen möchten (z. B. Ihre eigene "longtask.php"), können Sie die Verbindung von diesem Ende aus schließen, und beide Skripte werden parallel ausgeführt. Es funktioniert so:

  1. quick.php öffnet longtask.php über cURL (hier keine Magie)
  2. longtask.php schließt die Verbindung und fährt fort (Magie!)
  3. cURL kehrt zu quick.php zurück, wenn die Verbindung geschlossen wird
  4. Beide Aufgaben werden parallel fortgesetzt

Ich habe es versucht und es funktioniert gut. Quick.php weiß jedoch nichts darüber, wie longtask.php funktioniert, es sei denn, Sie erstellen ein Kommunikationsmittel zwischen den Prozessen.

Versuchen Sie diesen Code in longtask.php, bevor Sie etwas anderes tun. Die Verbindung wird geschlossen, aber weiterhin ausgeführt (und alle Ausgaben werden unterdrückt):

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

Der Code wurde aus den vom Benutzer bereitgestellten Notizen des PHP-Handbuchs kopiert und etwas verbessert.

Christian Davén
quelle
3
Das würde funktionieren. Wenn Sie jedoch ein MVC-Framework verwenden, kann die Implementierung schwierig sein, da diese Frameworks Aufrufe abfangen und neu schreiben. Zum Beispiel funktioniert es nicht in einem Controller in CakePHP
Chris Cinelli
Wenn Sie Zweifel an diesem Code haben, muss der Prozess, den Sie in Longtask ausführen müssen, nach diesen Zeilen ablaufen? Vielen Dank.
Morgar
Es funktioniert nicht perfekt. Versuchen Sie, while(true);nach Ihrem Code hinzuzufügen . Die Seite bleibt hängen, dh sie wird immer noch im Vordergrund ausgeführt.
زياد
17

Sie können Tricks ausführen, indem Sie exec () verwenden, um etwas aufzurufen, das HTTP-Anforderungen ausführen kann, z. B. wgetSie müssen jedoch die gesamte Ausgabe des Programms an einen Ort wie eine Datei oder / dev / null leiten, da sonst der PHP-Prozess auf diese Ausgabe wartet .

Wenn Sie den Prozess vollständig vom Apache-Thread trennen möchten, versuchen Sie Folgendes (ich bin mir nicht sicher, aber ich hoffe, Sie haben die Idee):

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

Es ist kein gutes Geschäft, und Sie möchten wahrscheinlich so etwas wie einen Cron-Job, der ein Heartbeat-Skript aufruft, das eine tatsächliche Datenbankereigniswarteschlange abfragt, um echte asynchrone Ereignisse auszuführen.

Internet-Freund
quelle
3
In ähnlicher Weise habe ich auch Folgendes getan: exec ("curl $ url> / dev / null &");
Matt Huggins
2
Frage: Gibt es einen Vorteil, wenn Sie "bash -c" wget "nennen und nicht nur" wget "?
Matt Huggins
2
In meinen Tests ist die Verwendung exec("curl $url > /dev/null 2>&1 &");hier eine der schnellsten Lösungen. Es ist immens schneller ( post_without_wait()1,9 s für 100 Iterationen) als die Funktion (14,8 s) in der obigen "akzeptierten" Antwort. UND es ist ein
Einzeiler
Verwenden Sie den vollständigen Pfad (z. B. / usr / bin / curl), um es noch schneller zu machen
Putnik
Wartet dies, bis das Skript fertig ist?
Cikatomo
11

Ab 2018 ist Guzzle die Defacto-Standardbibliothek für HTTP-Anforderungen, die in mehreren modernen Frameworks verwendet wird. Es ist in reinem PHP geschrieben und erfordert keine Installation von benutzerdefinierten Erweiterungen.

Es kann asynchrone HTTP-Aufrufe sehr gut ausführen und sie sogar bündeln, z. B. wenn Sie 100 HTTP-Aufrufe tätigen müssen, aber nicht mehr als 5 gleichzeitig ausführen möchten.

Beispiel für eine gleichzeitige Anforderung

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]

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

Simon East
quelle
3
Diese Antwort ist jedoch nicht asynchron. anscheinend macht guzzle das nicht
derlicious
2
Bei Guzzle müssen Sie die Locke installieren. Andernfalls ist es nicht parallel und gibt keine Warnung aus, dass es nicht parallel ist.
Velizar Hristov
Vielen Dank für den Link @daslicious - ja, es scheint, dass er nicht vollständig asynchron ist (wie in, wenn Sie eine Anfrage senden möchten, sich aber nicht um das Ergebnis kümmern), sondern ein paar Beiträge in diesem Thread, von dem ein Benutzer eine Problemumgehung angeboten hat Festlegen eines sehr niedrigen Anforderungszeitlimitwerts, der die Verbindungszeit weiterhin zulässt, aber nicht auf das Ergebnis wartet.
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
quelle
Dies ist nicht asynchron, da exec blockiert, bis Sie den Prozess beenden oder verzweigen, den Sie ausführen möchten.
Daniel W.
6
Hast du das &am Ende bemerkt ?
Philfreo
Würde dies dann das Skript blockieren oder nicht, ich bin verwirrt?
pleshy
1
@pleshy wird es nicht. kaufmännisches Und (&) bedeutet, das Skript im Hintergrund
auszuführen
8

Sie können diese Bibliothek verwenden: https://github.com/stil/curl-easy

Dann ist es ziemlich einfach:

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

Unten sehen Sie die Konsolenausgabe des obigen Beispiels. Es wird eine einfache Live-Uhr angezeigt, die angibt, wie viel Zeitanforderung ausgeführt wird:


Animation

Stil
quelle
Dies sollte die akzeptierte Antwort auf die Frage sein, denn selbst wenn es nicht wahr ist, dass es asynchron ist, ist es besser als die akzeptierte und alle "asynchronen" Antworten mit Guzzle (hier können Sie Operationen ausführen, während die Anfrage ausgeführt wird)
0ddlyoko
7
  1. Fake eine Anfrage Abtreibung mit CURLeinem Low setzenCURLOPT_TIMEOUT_MS

  2. so eingestellt ignore_user_abort(true), dass die Verarbeitung nach dem Schließen der Verbindung fortgesetzt wird.

Bei dieser Methode muss die Verbindungsbehandlung nicht über Header und Puffer implementiert werden, was zu stark von Betriebssystem, Browser und PHP-Version abhängt

Master-Prozess

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

Hintergrundprozess

ignore_user_abort(true);

//do something...

NB

Wenn Sie möchten, dass cURL in weniger als einer Sekunde eine Zeitüberschreitung aufweist, können Sie CURLOPT_TIMEOUT_MS verwenden, obwohl es einen Fehler / eine "Funktion" auf "Unix-ähnlichen Systemen" gibt, die dazu führt, dass libcurl sofort eine Zeitüberschreitung aufweist, wenn der Wert <1000 ms mit dem Fehler "ist. cURL-Fehler (28): Timeout wurde erreicht ". Die Erklärung für dieses Verhalten lautet:

[...]

Die Lösung besteht darin, Signale mit CURLOPT_NOSIGNAL zu deaktivieren

Ressourcen

RafaSashi
quelle
Wie gehen Sie mit dem Verbindungszeitlimit (Auflösung, DNS) um? Wenn ich timeout_ms auf 1 setze, bekomme ich immer "Zeitüberschreitung nach 4 ms auflösen" oder so ähnlich
Martin Wickman
Ich weiß es nicht, aber 4 ms klingen für mich schon ziemlich schnell ... Ich glaube nicht, dass Sie schneller auflösen können, indem Sie die Curl-Einstellungen ändern. Versuchen Sie vielleicht, die gezielte Anfrage zu optimieren ...
RafaSashi
Ok, aber timeout_ms = 1 legt das Timeout für die gesamte Anforderung fest. Wenn Ihre Auflösung länger als 1 ms dauert, tritt beim Curl eine Zeitüberschreitung auf und die Anforderung wird gestoppt. Ich sehe nicht, wie das überhaupt funktionieren kann (vorausgesetzt, die Auflösung dauert> 1 ms).
Martin Wickman
4

lass mich dir meinen Weg zeigen :)

benötigt nodejs auf dem Server installiert

(Mein Server sendet 1000 https Get-Anfrage dauert nur 2 Sekunden)

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
quelle
1
Bitte beachten Sie, dass viele Hosting-Anbieter die Verwendung bestimmter PHP-Funktionen (wie popen / exec ) nicht zulassen . Siehe PHP-Direktive disable_functions.
Eugen Mihailescu
4

Die Swoole-Erweiterung. https://github.com/matyhtf/swoole Asynchrones und gleichzeitiges Netzwerkframework für 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
quelle
4

Sie können nicht blockierende Sockets und eine der Pecl-Erweiterungen für PHP verwenden:

Sie können eine Bibliothek verwenden, die Ihnen eine Abstraktionsschicht zwischen Ihrem Code und einer pecl-Erweiterung bietet: https://github.com/reactphp/event-loop

Sie können auch einen asynchronen http-Client verwenden, der auf der vorherigen Bibliothek basiert: https://github.com/reactphp/http-client

Weitere Bibliotheken von ReactPHP finden Sie unter: http://reactphp.org

Seien Sie vorsichtig mit einem asynchronen Modell. Ich empfehle, dieses Video auf Youtube zu sehen: http://www.youtube.com/watch?v=MWNcItWuKpI

Roman Shamritskiy
quelle
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
quelle
2

Ereigniserweiterung

Event- Erweiterung ist sehr angemessen. Es ist ein Port der Libevent- Bibliothek, der für ereignisgesteuerte E / A, hauptsächlich für die Vernetzung, konzipiert ist.

Ich habe einen Beispiel-HTTP-Client geschrieben, mit dem eine Reihe von HTTP-Anforderungen geplant und asynchron ausgeführt werden können.

Dies ist eine Beispiel-HTTP-Clientklasse, die auf der Ereigniserweiterung basiert .

Die Klasse ermöglicht es, eine Reihe von HTTP-Anforderungen zu planen und diese dann asynchron auszuführen.

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

Dies ist ein Beispielskript auf der Serverseite.

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

Verwendung

php http-client.php

Beispielausgabe

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

(Beschnitten.)

Beachten Sie, dass der Code für die Langzeitverarbeitung in der CLI SAPI ausgelegt ist .


Erwägen Sie für benutzerdefinierte Protokolle die Verwendung einer API auf niedriger Ebene, dh Pufferereignisse , Puffer . Für die SSL / TLS-Kommunikation würde ich die Low-Level-API in Verbindung mit dem SSL-Kontext von Event empfehlen . Beispiele:


Obwohl die HTTP-API von Libevent einfach ist, ist sie nicht so flexibel wie Pufferereignisse. Beispielsweise unterstützt die HTTP-API derzeit keine benutzerdefinierten HTTP-Methoden. Es ist jedoch möglich, praktisch jedes Protokoll mithilfe der Low-Level-API zu implementieren.

Ev Erweiterung

Ich habe auch ein Beispiel eines anderen HTTP-Clients mit der Erweiterung Ev mit Sockets im nicht blockierenden Modus geschrieben . Der Code ist etwas ausführlicher als das auf Event basierende Beispiel, da Ev eine Allzweck-Ereignisschleife ist. Es bietet keine netzwerkspezifischen Funktionen, aber es istEvIo Watcher kann insbesondere einen in die Socket-Ressource gekapselten Dateideskriptor abhören.

Dies ist ein Beispiel für einen HTTP-Client, der auf der Ev- Erweiterung basiert .

Die Ev-Erweiterung implementiert eine einfache, aber leistungsstarke Allzweck-Ereignisschleife. Es bietet keine netzwerkspezifischen Beobachter, aber der E / A-Beobachter kann für die asynchrone Verarbeitung von Sockets verwendet werden .

Der folgende Code zeigt, wie HTTP-Anforderungen für die parallele Verarbeitung geplant werden können.

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

Testen

Angenommen, das http://my-host.local/test.phpSkript druckt den Speicherauszug von $_GET:

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

Dann ist die Ausgabe des php http-client.phpBefehls wie folgt:

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

(getrimmt)

Beachten Sie , dass in PHP 5 die Steckdosen - Erweiterung für log Warnungen können EINPROGRESS, EAGAINund EWOULDBLOCK errnoWerte. Es ist möglich, die Protokolle mit auszuschalten

error_reporting(E_ERROR);

In Bezug auf "den Rest" des Kodex

Ich möchte nur so etwas tun file_get_contents(), aber nicht warten, bis die Anforderung abgeschlossen ist, bevor ich den Rest meines Codes ausführe.

Der Code, der parallel zu den Netzwerkanforderungen ausgeführt werden soll, kann beispielsweise innerhalb des Rückrufs eines Ereignis-Timers oder des Leerlauf-Watchers von Ev ausgeführt werden . Sie können es leicht herausfinden, indem Sie sich die oben genannten Beispiele ansehen. Ansonsten füge ich noch ein Beispiel hinzu :)

Ruslan Osmanov
quelle
1

Hier ist ein funktionierendes Beispiel. Führen Sie es einfach aus und öffnen Sie die Datei storage.txt, um das magische Ergebnis zu überprüfen

<?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
quelle
1

Hier ist meine eigene PHP-Funktion, wenn ich POST zu einer bestimmten URL einer beliebigen Seite mache .... Beispiel: *** Verwendung meiner Funktion ...

    <?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>
Ich bin ArbZ
quelle
1

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

Installation über 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();

Führen Sie PHP im CLI-Modus aus

$ php get.php
Sergey Shuchkin
quelle
0

Ich finde dieses Paket sehr nützlich und sehr einfach: 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);
}));

Es werden alle 3 URLs parallel geladen. Sie können im Abschluss auch Klasseninstanzmethoden verwenden.

Zum Beispiel verwende ich die Laravel-Erweiterung basierend auf diesem Paket https://github.com/spatie/laravel-collection-macros#parallelmap

Hier ist mein 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);
    }

Es lädt alle benötigten Daten in 10 parallelen Threads und statt 50 Sekunden ohne Asynchronisierung in nur 8 Sekunden.

Vedmant
quelle
0

Symfony HttpClient ist asynchron https://symfony.com/doc/current/components/http_client.html .

Zum Beispiel können Sie

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
quelle
-4

Das Zeitlimit kann in Millisekunden festgelegt werden, siehe "CURLOPT_CONNECTTIMEOUT_MS" unter http://www.php.net/manual/en/function.curl-setopt

Akhil Sikri
quelle
3
Es war nur eine Kappe, die an eine Auszeit dachte. Es ist überhaupt nicht asynchron.
Chris Cinelli