Jak wykonywać asynchroniczne żądania HTTP w PHP

209

Czy w PHP jest sposób wykonywania asynchronicznych wywołań HTTP? Nie dbam o odpowiedź, chcę po prostu zrobić coś takiego file_get_contents(), ale nie czekam na zakończenie żądania przed wykonaniem reszty mojego kodu. Byłoby to bardzo przydatne do wywoływania „zdarzeń” w mojej aplikacji lub uruchamiania długich procesów.

Jakieś pomysły?

Brent
źródło
9
jedna funkcja - 'curl_multi', poszukaj jej w dokumentacji php. Powinien rozwiązać twoje problemy
James Butler
22
Tytuł tego posta wprowadza w błąd. Przyszedłem szukać prawdziwie asynchronicznych wywołań podobnych do żądań w Node.js lub AJAX. Akceptowana odpowiedź nie jest asynchroniczna (blokuje i nie zapewnia oddzwaniania), a jedynie szybsze żądanie synchroniczne. Rozważ zmianę pytania lub zaakceptowanej odpowiedzi.
Johntron
Zabawa z obsługą połączeń przez nagłówki i bufor nie jest kuloodporna. Właśnie opublikowałem nową odpowiedź niezależną od systemu operacyjnego, przeglądarki lub wersji PHP
RafaSashi
1
Asynchroniczny nie oznacza, że ​​nie przejmujesz się odpowiedzią. Oznacza to po prostu, że wywołanie nie blokuje wykonania głównego wątku. Asynchroniczny nadal wymaga odpowiedzi, ale odpowiedź może być przetwarzana w innym wątku wykonania lub później w pętli zdarzeń. To pytanie dotyczy żądania pożaru i zapomnienia, które może być synchroniczne lub asynchroniczne w zależności od semantyki dostarczania wiadomości, niezależnie od tego, czy zależy Ci na kolejności wiadomości, czy na potwierdzeniu dostarczenia.
CMCDragonkai
Myślę, że powinieneś wysłać to żądanie HTTP w trybie nieblokującym (w / c jest tym, czego naprawdę chcesz). Ponieważ gdy wywołujesz zasób, w zasadzie chcesz wiedzieć, czy osiągnąłeś serwer, czy nie (lub z dowolnego powodu, po prostu potrzebujesz odpowiedzi). Najlepszą odpowiedzią jest fsockopen i ustawienie odczytu lub zapisu strumienia na tryb nieblokujący. To jak dzwonić i zapomnieć.
KiX Ortillan

Odpowiedzi:

42

Odpowiedź, którą wcześniej zaakceptowałem, nie zadziałała. Nadal czekał na odpowiedzi. To jednak działa, wzięte z Jak wykonać asynchroniczne żądanie GET w 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
źródło
67
To NIE jest asynchronizacja! W szczególności, jeśli serwer po drugiej stronie jest wyłączony, ten fragment kodu zawiesi się na 30 sekund (piąty parametr w fsockopen). Również fwrite poświęci swój słodki czas na wykonanie (które możesz ograniczyć za pomocą stream_set_timeout ($ fp, $ my_timeout). Najlepsze, co możesz zrobić, to ustawić niski limit czasu na fsockopen na 0,1 (100ms) i $ my_timeout na 100ms Ryzykujesz jednak, że upłynął limit czasu żądania
Chris Cinelli
3
Zapewniam cię, że jest asynchroniczny i nie zajmuje 30 sekund. To maksymalny limit czasu. Możliwe, że Twoje ustawienia są różne, powodując ten efekt, ale działało to dla mnie świetnie.
Brent,
11
@UltimateBrent W kodzie nie ma nic, co sugerowałoby, że jest on asynchroniczny. Nie czeka na odpowiedź, ale to nie jest asynchroniczne. Jeśli serwer zdalny otworzy połączenie, a następnie zawiesi się, ten kod będzie czekać 30 sekund, aż upłynie limit czasu.
chmac
17
powód, dla którego wydaje się działać „asynchronicznie”, ponieważ nie czytasz z gniazda przed zamknięciem, więc nie zawiesił się, nawet jeśli serwer nie wyemitował odpowiedzi na czas. Jednak nie jest to absolutnie asynchroniczne. Jeśli bufor zapisu jest pełny (co najmniej prawdopodobne), skrypt na pewno się tam zawiesi. Zastanów się nad zmianą tytułu na „prośba o stronę internetową bez oczekiwania na odpowiedź”.
howanghk
3
To nie jest ani asynchronizacja, ani używanie curl, jak śmiesz nazywać to curl_post_asynci zdobywać nawet głosy poparcia ...
Daniel W.
27

Jeśli kontrolujesz cel, który chcesz wywołać asynchronicznie (np. Własny „longtask.php”), możesz zamknąć połączenie z tego końca, a oba skrypty będą działały równolegle. Działa to tak:

  1. quick.php otwiera longtask.php przez cURL (tutaj nie ma magii)
  2. longtask.php zamyka połączenie i kontynuuje (magia!)
  3. cURL powraca do quick.php po zamknięciu połączenia
  4. Oba zadania są kontynuowane równolegle

Próbowałem tego i działa dobrze. Ale quick.php nie będzie wiedział nic o tym, jak radzi sobie longtask.php, chyba że stworzysz jakieś środki komunikacji między procesami.

Wypróbuj ten kod w longtask.php, zanim zrobisz cokolwiek innego. Zamknie połączenie, ale nadal będzie działać (i pomija wszelkie dane wyjściowe):

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

Kod jest kopiowany z notatek przesłanych przez użytkownika instrukcji PHP i nieco ulepszony.

Christian Davén
źródło
3
To by działało. Ale jeśli używasz frameworka MVC, jego implementacja może być trudna, ponieważ sposób, w jaki frameworki te przechwytują i przepisują połączenia. Na przykład nie działa w kontrolerze w CakePHP
Chris Cinelli
Wątpliwości dotyczące tego kodu, proces, który musisz wykonać w zadaniu długoterminowym, musi przebiegać po tych wierszach? Dzięki.
morgar
To nie działa idealnie. Spróbuj dodać while(true);po kodzie. Strona się zawiesi, co oznacza, że ​​nadal działa na pierwszym planie.
زياد
17

Możesz zrobić oszustwo, używając exec (), aby wywołać coś, co może wykonywać żądania HTTP, na przykład wget, ale musisz przekierować gdzieś wszystkie dane wyjściowe z programu, takie jak plik lub / dev / null, w przeciwnym razie proces PHP będzie czekać na to wyjście .

Jeśli chcesz całkowicie oddzielić proces od wątku apache, spróbuj czegoś takiego (nie jestem tego pewien, ale mam nadzieję, że masz pomysł):

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

To nie jest fajna sprawa i prawdopodobnie będziesz potrzebować czegoś takiego jak zadanie cron wywołujące skrypt pulsu, który odpytuje rzeczywistą kolejkę zdarzeń bazy danych w celu wykonania prawdziwych zdarzeń asynchronicznych.

Przyjaciel Internetu
źródło
3
Podobnie zrobiłem również: exec ("curl $ url> / dev / null &");
Matt Huggins,
2
Pytanie: czy jest korzyść z nazywania „bash -c” wget ”zamiast zwykłego„ wget ”?
Matt Huggins
2
W moich testach używanie exec("curl $url > /dev/null 2>&1 &");jest jednym z najszybszych rozwiązań. Jest niesamowicie szybszy (1,9 na 100 iteracji) niż post_without_wait()funkcja (14,8 s) w „zaakceptowanej” odpowiedzi powyżej. I to jest jedno-liniowiec ...
rinogo
Użyj pełnej ścieżki (np. / Usr / bin / curl), aby uczynić ją jeszcze szybszą
Putnik
czy to czeka, aż skrypt się zakończy?
cikatomo
11

Od 2018 roku Guzzle stał się standardową biblioteką defacto dla żądań HTTP, używaną w kilku nowoczesnych frameworkach. Jest napisany w czystym PHP i nie wymaga instalowania żadnych niestandardowych rozszerzeń.

Może bardzo ładnie wykonywać asynchroniczne połączenia HTTP, a nawet łączyć je, np. Gdy trzeba wykonać 100 połączeń HTTP, ale nie chce się uruchamiać więcej niż 5 na raz.

Przykład równoczesnego żądania

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]

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

Simon East
źródło
3
Jednak ta odpowiedź nie jest asynchroniczna. najwyraźniej żubr tego nie robi
oszukańczy
2
Pysk wymaga zainstalowania loków. W przeciwnym razie nie jest równoległy i nie daje ostrzeżenia, że ​​nie jest równoległy.
Velizar Hristov
Dzięki za link @daslicious - tak, wygląda na to, że nie jest całkowicie asynchroniczny (jak w przypadku, gdy chcesz wysłać prośbę, ale nie dbasz o wynik), ale kilka postów w tym wątku użytkownik zaproponował obejście przez ustawienie bardzo niskiej wartości limitu czasu żądania, która nadal dopuszcza czas połączenia, ale nie czeka na wynik.
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
źródło
Nie jest to asynchroniczne, ponieważ exec blokuje, dopóki nie zamkniesz lub nie rozwidlisz procesu, który chcesz uruchomić.
Daniel W.
6
Czy zauważyłeś &koniec?
philfreo,
Czy to blokowałoby wtedy skrypt, czy nie, jestem zdezorientowany?
mięsisty
1
@pleshy nie będzie. ampersand (&) oznacza uruchomienie skryptu w tle
daisura99
8

Możesz użyć tej biblioteki: https://github.com/stil/curl-easy

Jest to więc dość proste:

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

Poniżej możesz zobaczyć wyjście konsoli z powyższego przykładu. Wyświetli prosty zegar na żywo wskazujący, ile czasu trwa żądanie:


animacja

stil
źródło
Powinna to być zaakceptowana odpowiedź na pytanie, ponieważ nawet jeśli nie jest to prawdziwa asynchronizacja, jest lepsza niż zaakceptowana i wszystkie odpowiedzi „asynchroniczne” z żonglowaniem (tutaj możesz wykonywać operacje podczas wykonywania żądania)
0ddlyoko
7
  1. Fałszywa aborcja przy użyciu CURLustawienia niskiegoCURLOPT_TIMEOUT_MS

  2. ustawione ignore_user_abort(true)na kontynuowanie przetwarzania po zamknięciu połączenia.

Dzięki tej metodzie nie trzeba wdrażać obsługi połączeń za pośrednictwem nagłówków i buforów zbyt zależnych od wersji systemu operacyjnego, przeglądarki i PHP

Proces główny

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

Proces w tle

ignore_user_abort(true);

//do something...

NB

Jeśli chcesz, aby czas CURL upłynął w czasie krótszym niż jedna sekunda, możesz użyć CURLOPT_TIMEOUT_MS, chociaż w „systemach uniksowych” występuje błąd / „funkcja”, która powoduje, że libcurl natychmiast przestaje działać, jeśli wartość wynosi <1000 ms z błędem „ Błąd cURL (28): Osiągnięto limit czasu ”. Wyjaśnienie tego zachowania jest następujące:

[...]

Rozwiązaniem jest wyłączenie sygnałów za pomocą CURLOPT_NOSIGNAL

Zasoby

RafaSashi
źródło
Jak radzisz sobie z przekroczeniem limitu czasu połączenia (rozwiązanie, dns)? Kiedy ustawiam timeout_ms na 1, zawsze kończę na „rozwiązaniu upłynął limit czasu po 4 ms” lub coś w tym rodzaju
Martin Wickman
Nie wiem, ale 4 ms brzmi dla mnie już dość szybko ... Nie sądzę, że można to rozwiązać szybciej, zmieniając ustawienia curl. Spróbuj zoptymalizować ukierunkowane żądanie, być może ...
RafaSashi,
Ok, ale timeout_ms = 1 ustawia limit czasu dla całego żądania. Jeśli więc rozwiązanie zajmie więcej niż 1 ms, curl przekroczy limit czasu i zatrzyma żądanie. Nie rozumiem, jak to może w ogóle działać (przy założeniu, że rozwiązanie zajmie> 1 ms).
Martin Wickman,
4

pozwól, że pokażę ci moją drogę

wymaga zainstalowania nodejs na serwerze

(mój serwer wysyła 1000 próśb o pobranie https zajmuje tylko 2 sekundy)

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);
}
użytkownik1031143
źródło
1
Należy pamiętać, że wielu dostawców hostingu nie zezwala na korzystanie z niektórych funkcji PHP (takich jak popen / exec ). Zobacz dyrektywę PHP disable_functions.
Eugen Mihailescu,
4

Rozszerzenie swoole. https://github.com/matyhtf/swoole Struktura asynchroniczna i współbieżna dla 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
źródło
4

Możesz używać nieblokujących gniazd i jednego z rozszerzeń pecl dla PHP:

Możesz użyć biblioteki, która daje warstwę abstrakcji między twoim kodem a rozszerzeniem pecl: https://github.com/reactphp/event-loop

Możesz także użyć asynchronicznego klienta HTTP opartego na poprzedniej bibliotece: https://github.com/reactphp/http-client

Zobacz inne biblioteki ReactPHP: http://reactphp.org

Ostrożnie z modelem asynchronicznym. Polecam zobaczyć ten film na youtube: http://www.youtube.com/watch?v=MWNcItWuKpI

Roman Shamritskiy
źródło
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
źródło
2

Rozszerzenie zdarzenia

Rozszerzenie zdarzenia jest bardzo odpowiednie. Jest to port biblioteki Libevent, która jest przeznaczona do sterowanych zdarzeniami operacji we / wy, głównie do pracy w sieci.

Napisałem przykładowego klienta HTTP, który pozwala zaplanować szereg żądań HTTP i uruchomić je asynchronicznie.

To jest przykładowa klasa klienta HTTP oparta na rozszerzeniu Event .

Klasa pozwala zaplanować szereg żądań HTTP, a następnie uruchomić je asynchronicznie.

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

To jest przykładowy skrypt po stronie serwera.

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

Stosowanie

php http-client.php

Przykładowe dane wyjściowe

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

(Przycięty.)

Uwaga: kod jest przeznaczony do długoterminowego przetwarzania w interfejsie CLI SAPI .


W przypadku protokołów niestandardowych rozważ użycie interfejsu API niskiego poziomu, tj. Zdarzeń bufora , buforów . Do komunikacji SSL / TLS polecam interfejs API niskiego poziomu w połączeniu z kontekstem ssl zdarzenia . Przykłady:


Chociaż API HTTP Libevent jest proste, nie jest tak elastyczne jak zdarzenia buforujące. Na przykład interfejs API HTTP obecnie nie obsługuje niestandardowych metod HTTP. Możliwe jest jednak wdrożenie praktycznie dowolnego protokołu przy użyciu niskopoziomowego interfejsu API.

Rozszerzenie Ev

Napisałem również próbkę innego klienta HTTP używającego rozszerzenia Ev z gniazdami w trybie nieblokującym . Kod jest nieco bardziej szczegółowy niż próbka oparta na zdarzeniu, ponieważ Ev jest pętlą zdarzeń ogólnego przeznaczenia. Nie zapewnia funkcji specyficznych dla sieci, ale jestEvIo obserwator może w szczególności nasłuchiwać deskryptora pliku zawartego w zasobie gniazda.

To jest przykładowy klient HTTP oparty na rozszerzeniu Ev .

Rozszerzenie Ev implementuje prostą, ale potężną pętlę zdarzeń ogólnego przeznaczenia. Nie zapewnia obserwatorów specyficznych dla sieci, ale jego obserwator we / wy może być używany do asynchronicznego przetwarzania gniazd .

Poniższy kod pokazuje, jak można zaplanować żądania HTTP do przetwarzania równoległego.

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

Testowanie

Załóżmy, że http://my-host.local/test.phpskrypt wypisuje zrzut $_GET:

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

Następnie dane wyjściowe php http-client.phppolecenia będą podobne do następujących:

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

(przycięte)

Uwaga, w PHP 5 gniazd rozszerzeń może ostrzeżenia dla zalogować EINPROGRESS, EAGAINi EWOULDBLOCK errnowartości. Można wyłączyć dzienniki za pomocą

error_reporting(E_ERROR);

Dotyczące „reszty” Kodeksu

Chcę po prostu zrobić coś takiego file_get_contents(), ale nie czekam na zakończenie żądania przed wykonaniem reszty mojego kodu.

Kod, który ma działać równolegle z żądaniami sieci, może zostać wykonany na przykład w ramach wywołania zwrotnego timera zdarzeń lub bezczynnego obserwatora Eva . Możesz łatwo to rozgryźć, oglądając próbki wspomniane powyżej. W przeciwnym razie dodam kolejny przykład :)

Ruslan Osmanov
źródło
1

Oto działający przykład, po prostu uruchom go i otwórz storage.txt, aby sprawdzić magiczny wynik

<?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
źródło
1

Oto moja własna funkcja PHP, gdy wykonuję POST pod konkretnym adresem URL dowolnej strony .... Przykład: *** użycie mojej funkcji ...

    <?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>
jestem ArbZ
źródło
1

Klient HTTP ReactPHP asynchroniczny
https://github.com/shuchkin/react-http-client

Zainstaluj przez Composer

$ composer require shuchkin/react-http-client

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

Uruchom php w trybie CLI

$ php get.php
Siergiej Szuchkin
źródło
0

Uważam ten pakiet za bardzo użyteczny i bardzo prosty: 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);
}));

Załaduje wszystkie 3 adresy URL równolegle. Możesz również użyć metod instancji klasy w zamknięciu.

Na przykład używam rozszerzenia Laravel opartego na tym pakiecie https://github.com/spatie/laravel-collection-macros#parallelmap

Oto mój kod:

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

Ładuje wszystkie potrzebne dane w 10 równoległych wątkach i zamiast 50 sekund bez asynchronizacji zakończyło się w zaledwie 8 sekund.

Vedmant
źródło
0

Symfony HttpClient jest asynchroniczny https://symfony.com/doc/current/components/http_client.html .

Na przykład możesz

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
źródło
-4

Limit czasu można ustawić w milisekundach, patrz „CURLOPT_CONNECTTIMEOUT_MS” w http://www.php.net/manual/en/function.curl-setopt

Akhil Sikri
źródło
3
Doprowadziło to do limitu czasu. To wcale nie jest asynchroniczne.
Chris Cinelli