How to make asynchronous HTTP requests in PHP

后端 未结 18 2033
梦如初夏
梦如初夏 2020-11-22 02:13

Is there a way in PHP to make asynchronous HTTP calls? I don\'t care about the response, I just want to do something like file_get_contents(), but not wait for

相关标签:
18条回答
  • 2020-11-22 02:53

    Symfony HttpClient is asynchronous https://symfony.com/doc/current/components/http_client.html.

    For example you can

    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
    
    0 讨论(0)
  • 2020-11-22 02:55

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

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

    Run php in CLI-mode

    $ php get.php
    
    0 讨论(0)
  • 2020-11-22 03:03

    Event Extension

    Event extension is very appropriate. It is a port of Libevent library which is designed for event-driven I/O, mainly for networking.

    I have written a sample HTTP client that allows to schedule a number of HTTP requests and run them asynchronously.

    This is a sample HTTP client class based on Event extension.

    The class allows to schedule a number of HTTP requests, then run them asynchronously.

    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

    This is a sample script on the server side.

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

    Usage

    php http-client.php
    

    Sample Output

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

    (Trimmed.)

    Note, the code is designed for long-term processing in the CLI SAPI.


    For custom protocols, consider using low-level API, i.e. buffer events, buffers. For SSL/TLS communications, I would recommend the low-level API in conjunction with Event's ssl context. Examples:

    • SSL echo server
    • SSL client

    Although Libevent's HTTP API is simple, it is not as flexible as buffer events. For example, the HTTP API currently doesn't support custom HTTP methods. But it is possible to implement virtually any protocol using the low-level API.

    Ev Extension

    I have also written a sample of another HTTP client using Ev extension with sockets in non-blocking mode. The code is slightly more verbose than the sample based on Event, because Ev is a general purpose event loop. It doesn't provide network-specific functions, but its EvIo watcher is capable of listening to a file descriptor encapsulated into the socket resource, in particular.

    This is a sample HTTP client based on Ev extension.

    Ev extension implements a simple yet powerful general purpose event loop. It doesn't provide network-specific watchers, but its I/O watcher can be used for asynchronous processing of sockets.

    The following code shows how HTTP requests can be scheduled for parallel processing.

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

    Testing

    Suppose http://my-host.local/test.php script is printing the dump of $_GET:

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

    Then the output of php http-client.php command will be similar to the following:

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

    (trimmed)

    Note, in PHP 5 the sockets extension may log warnings for EINPROGRESS, EAGAIN, and EWOULDBLOCK errno values. It is possible to turn off the logs with

    error_reporting(E_ERROR);
    

    Concerning "the Rest" of the Code

    I just want to do something like file_get_contents(), but not wait for the request to finish before executing the rest of my code.

    The code that is supposed to run in parallel with the network requests can be executed within a the callback of an Event timer, or Ev's idle watcher, for instance. You can easily figure it out by watching the samples mentioned above. Otherwise, I'll add another example :)

    0 讨论(0)
  • 2020-11-22 03:03

    Well, the timeout can be set in milliseconds, see "CURLOPT_CONNECTTIMEOUT_MS" in http://www.php.net/manual/en/function.curl-setopt

    0 讨论(0)
  • 2020-11-22 03:04

    If you control the target that you want to call asynchronously (e.g. your own "longtask.php"), you can close the connection from that end, and both scripts will run in parallel. It works like this:

    1. quick.php opens longtask.php via cURL (no magic here)
    2. longtask.php closes the connection and continues (magic!)
    3. cURL returns to quick.php when the connection is closed
    4. Both tasks continue in parallel

    I have tried this, and it works just fine. But quick.php won't know anything about how longtask.php is doing, unless you create some means of communication between the processes.

    Try this code in longtask.php, before you do anything else. It will close the connection, but still continue to run (and suppress any output):

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

    The code is copied from the PHP manual's user contributed notes and somewhat improved.

    0 讨论(0)
  • 2020-11-22 03:05

    You can do trickery by using exec() to invoke something that can do HTTP requests, like wget, but you must direct all output from the program to somewhere, like a file or /dev/null, otherwise the PHP process will wait for that output.

    If you want to separate the process from the apache thread entirely, try something like (I'm not sure about this, but I hope you get the idea):

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

    It's not a nice business, and you'll probably want something like a cron job invoking a heartbeat script which polls an actual database event queue to do real asynchronous events.

    0 讨论(0)
提交回复
热议问题