How to make asynchronous HTTP requests in PHP

后端 未结 18 2045
梦如初夏
梦如初夏 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 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

    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.

    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

    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:

    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 :)

提交回复
热议问题