Как сделать асинхронные HTTP-запросы в PHP


209

Есть ли в PHP способ сделать асинхронные HTTP-вызовы? Меня не волнует ответ, я просто хочу сделать что-то подобное file_get_contents(), но не ждать, пока запрос завершится, прежде чем выполнить остальную часть моего кода. Это было бы очень полезно для отключения "событий" в моем приложении или запуска длинных процессов.

Любые идеи?


9
одна функция - curl_multi, посмотрите в документации по php. Должны решить ваши проблемы
Джеймс Батлер

22
Название этого сообщения вводит в заблуждение. Я искал действительно асинхронные вызовы, похожие на запросы в Node.js или запрос AJAX. Принятый ответ не асинхронный (он блокирует и не обеспечивает обратный вызов), просто более быстрый синхронный запрос. Подумайте об изменении вопроса или принятого ответа.
Johntron

Игра с обработкой соединения через заголовки и буфер не является пуленепробиваемой. Я только что опубликовал новый ответ, независимый от версии ОС, браузера или PHP
RafaSashi

1
Асинхронный не означает, что вы не заботитесь об ответе. Это просто означает, что вызов не блокирует выполнение основного потока. Асинхронный по-прежнему требует ответа, но ответ может быть обработан в другом потоке выполнения или позже в цикле событий. Этот вопрос задает запрос на запуск и забытие, который может быть синхронным или асинхронным в зависимости от семантики доставки сообщений, от того, заботитесь ли вы о порядке сообщений или подтверждении доставки.
CMCDragonkai

Я думаю, что вы должны сделать этот HTTP-запрос на запуск в неблокирующем режиме (w / c - это то, что вы действительно хотите). Потому что, когда вы вызываете ресурс, вы в основном хотите знать, достиг ли вы сервера или нет (или по любой другой причине, вам просто нужен ответ). Лучший ответ на самом деле - fsockopen и установка потокового чтения или записи в неблокирующий режим. Это как позвонить и забыть.
KiX Ortillan

Ответы:


42

Ответ, который я ранее принял, не сработал. Он все еще ждал ответов. Это работает, хотя, взято из Как сделать асинхронный запрос GET в 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);
}

67
Это НЕ асинхронно! В частности, если сервер на другой стороне не работает, этот фрагмент кода будет зависать в течение 30 секунд (5-й параметр в fsockopen). Кроме того, fwrite будет занимать свое приятное время для выполнения (которое вы можете ограничить с помощью stream_set_timeout ($ fp, $ my_timeout). Лучшее, что вы можете сделать, это установить для тайм-аута fsockopen значение 0,1 (100 мс) и $ my_timeout до 100 мс. Хотя вы рискуете, что время ожидания запроса истекло
Крис Синелли

3
Уверяю вас, что он асинхронный и не занимает 30 секунд. Это тайм-аут макс. Возможно, что ваши настройки отличаются, вызывая этот эффект, но это отлично сработало для меня.
Brent

11
@UltimateBrent В коде нет ничего, что предполагало бы его асинхронность. Он не ждет ответа, но он не асинхронный. Если удаленный сервер открывает соединение, а затем зависает, этот код будет ждать 30 секунд, пока не истечет время ожидания.
Chmac

17
причина в том, что он работает "асинхронно", потому что вы не читаете из сокета перед тем, как закрыть его, чтобы он не зависал, даже если сервер не выдал ответ вовремя. Однако это абсолютно не асинхронно. Если буфер записи заполнен (очень маловероятно), ваш скрипт обязательно зависнет там. Вам следует подумать об изменении названия на что-то вроде «запрос веб-страницы без ожидания ответа».
Howanghk

3
Это не асинхронно и не использует curl, как ты смеешь называть это curl_post_asyncи получать даже голоса ...
Даниэль В.

27

Если вы контролируете цель, которую хотите вызвать асинхронно (например, свой собственный "longtask.php"), вы можете закрыть соединение с этой стороны, и оба сценария будут работать параллельно. Это работает так:

  1. quick.php открывает longtask.php через cURL (никакой магии здесь)
  2. longtask.php закрывает соединение и продолжает (магия!)
  3. cURL возвращается к quick.php, когда соединение закрыто
  4. Обе задачи продолжаются параллельно

Я попробовал это, и это работает просто отлично. Но quick.php не будет ничего знать о том, как работает longtask.php, если вы не создадите какое-либо средство связи между процессами.

Попробуйте этот код в longtask.php, прежде чем делать что-либо еще. Это закроет соединение, но все еще продолжит работать (и подавит любой вывод):

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

Код скопирован из заметок пользователя руководства PHP и несколько улучшен.


3
Это будет работать. Но если вы используете инфраструктуру MVC, это может быть трудно реализовать из-за способа, которым эти структуры перехватывают и перезаписывают вызовы. Например, он не работает в контроллере в CakePHP
Крис Синелли

Сомнение по поводу этого кода, процесс, который вам нужно сделать в longtask, должен идти после этих строк? Спасибо.
Моргар

Это не работает идеально. Попробуйте добавить while(true);после вашего кода. Страница будет зависать, это означает, что она все еще работает на переднем плане.
ياد

17

Вы можете сделать обман, используя exec () для вызова чего-то, что может выполнять HTTP-запросы, например wget, но вы должны перенаправить весь вывод из программы куда-нибудь, например, в файл или / dev / null, в противном случае процесс PHP будет ожидать этого вывода ,

Если вы хотите полностью отделить процесс от потока apache, попробуйте что-то вроде (я не уверен в этом, но надеюсь, вы поняли идею):

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

Это не очень хороший бизнес, и вам, вероятно, понадобится что-то вроде задания cron, вызывающего скрипт heartbeat, который опрашивает фактическую очередь событий базы данных, чтобы выполнить реальные асинхронные события.


3
Точно так же я сделал следующее: exec ("curl $ url> / dev / null &");
Мэтт Хаггинс

2
Вопрос: есть ли смысл называть bash -c "wget", а не просто "wget"?
Мэтт Хаггинс

2
В моем тестировании использование exec("curl $url > /dev/null 2>&1 &");является одним из самых быстрых решений здесь. Это намного быстрее (1,9 с на 100 итераций), чем post_without_wait()функция (14,8 с) в «принятом» ответе выше. И это однострочник ...
Риного

Используйте полный путь (например , / USR / бен / локон) , чтобы сделать его еще более быстрее
Путник

это ждет, пока скрипт не закончится?
cikatomo

12

Начиная с 2018 года, Guzzle стала стандартной библиотекой defacto для HTTP-запросов, используемой в нескольких современных средах. Он написан на чистом PHP и не требует установки каких-либо пользовательских расширений.

Он может очень хорошо выполнять асинхронные HTTP-вызовы и даже объединять их в группы, например, когда вам нужно выполнить 100 HTTP-вызовов, но не хотите выполнять более 5 одновременно.

Пример параллельного запроса

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]

См. Http://docs.guzzlephp.org/en/stable/quickstart.html#concurrent-requests.


3
Однако этот ответ не асинхронный. очевидно жрет не делает этого
daslicious

2
Жрет требует от вас установить локон. В противном случае он не параллелен, и он не дает вам никакого предупреждения, что он не параллелен.
Велизар Христов

Спасибо за ссылку @daslicious - да, похоже, что она не полностью асинхронна (например, когда вы хотите отправить запрос, но не заботитесь о результате), но несколько сообщений в этой ветке пользователь предложил для обхода установка очень низкого значения тайм-аута запроса, которое все еще разрешает время соединения, но не ждет результата.
Саймон Ист

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 &");
}

Это не асинхронно, потому что exec блокирует, пока вы не завершите или не разветвите процесс, который хотите запустить.
Даниэль В.

6
Вы заметили &в конце?
Philfreo

Так будет ли это блокировать сценарий или нет, я в замешательстве?
15:33

1
@ Плеш это не будет. амперсанд (&) означает запуск сценария в фоновом режиме
daisura99

8

Вы можете использовать эту библиотеку: https://github.com/stil/curl-easy

Это довольно просто тогда:

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

Ниже вы можете увидеть вывод консоли из приведенного выше примера. Он будет отображать простые живые часы, показывающие, сколько времени выполняется запрос:


анимация


Это должен быть принятый ответ на вопрос, потому что, даже если он не является истинным асинхронным, он лучше, чем принятый один и все «асинхронные» ответы с жадностью (Здесь вы можете выполнять операции, пока выполняется запрос)
0ddlyoko

7
  1. Подделка запроса на прерывание беременности с помощью CURLустановки низкогоCURLOPT_TIMEOUT_MS

  2. установить ignore_user_abort(true)сохранение обработки после закрытия соединения.

При использовании этого метода не требуется реализовывать обработку соединения через заголовки и буфер, слишком зависящий от ОС, браузера и версии PHP

Мастер процесс

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

Фоновый процесс

ignore_user_abort(true);

//do something...

NB

Если вы хотите, чтобы cURL превысил время ожидания менее чем за одну секунду, вы можете использовать CURLOPT_TIMEOUT_MS, хотя в «Unix-подобных системах» есть ошибка / «функция», которая приводит к немедленному истечению времени ожидания libcurl, если значение <1000 мс с ошибкой » Ошибка cURL (28): истекло время ожидания ". Объяснение этого поведения:

[...]

Решение состоит в том, чтобы отключить сигналы, используя CURLOPT_NOSIGNAL

Ресурсы


Как вы обрабатываете время ожидания соединения (решите, днс)? Когда я устанавливаю timeout_ms равным 1, я всегда получаю «разрешение по истечении 4 мсек» или что-то в этом роде
Martin

Я не знаю, но 4 мс звучат уже довольно быстро для меня ... Я не думаю, что вы можете решить быстрее, изменив любые настройки завитка. Попробуйте оптимизировать целевой запрос, возможно ...
RafaSashi

Хорошо, но timeout_ms = 1 устанавливает время ожидания для всего запроса. Таким образом, если ваше разрешение занимает более 1 мс, то curl прекратит работу и остановит запрос. Я не вижу, как это может работать вообще (при условии, что разрешение занимает> 1 мс).
Мартин

4

позвольте мне показать вам мой путь :)

нужен nodejs, установленный на сервере

(мой сервер отправляет 1000 запросов на получение https занимает всего 2 секунды)

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

1
Обратите внимание, что многие хостинг-провайдеры не разрешают использовать определенные функции PHP (например, popen / exec ). Смотрите PHP-директиву disable_functions.
Евгений Михайлеску

4

Расширение swoole. https://github.com/matyhtf/swoole Асинхронная и параллельная сетевая среда для 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);

4

Вы можете использовать неблокирующие сокеты и одно из расширений pecl для PHP:

Вы можете использовать библиотеку, которая предоставляет вам слой абстракции между вашим кодом и расширением pecl: https://github.com/reactphp/event-loop

Вы также можете использовать асинхронный http-клиент на основе предыдущей библиотеки: https://github.com/reactphp/http-client

Смотрите другие библиотеки ReactPHP: http://reactphp.org

Будьте осторожны с асинхронной моделью. Я рекомендую посмотреть это видео на YouTube: http://www.youtube.com/watch?v=MWNcItWuKpI


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

2

Расширение событий

Расширение событий очень уместно. Это порт библиотеки Libevent, который предназначен для ввода-вывода, управляемого событиями, в основном для работы в сети.

Я написал пример HTTP-клиента, который позволяет планировать количество HTTP-запросов и выполнять их асинхронно.

Это пример класса HTTP-клиента, основанный на расширении Event .

Класс позволяет планировать количество HTTP-запросов, а затем выполнять их асинхронно.

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

Это пример сценария на стороне сервера.

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

использование

php http-client.php

Пример вывода

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

(Стрижка.)

Обратите внимание, что код предназначен для долгосрочной обработки в CLI SAPI .


Для пользовательских протоколов рассмотрите использование низкоуровневого API, то есть буферных событий , буферов . Для связи SSL / TLS я бы рекомендовал низкоуровневый API в сочетании с контекстом ssl Event . Примеры:


Хотя HTTP-интерфейс Libevent прост, он не так гибок, как буферные события. Например, HTTP API в настоящее время не поддерживает пользовательские методы HTTP. Но можно реализовать практически любой протокол, используя низкоуровневый API.

Расширение Ev

Я также написал пример другого HTTP-клиента, использующего расширение Ev с сокетами в неблокирующем режиме . Код немного более подробный, чем пример, основанный на Event, потому что Ev является циклом событий общего назначения. Он не предоставляет специфичных для сети функций, но его EvIoнаблюдатель способен, в частности, прослушивать дескриптор файла, инкапсулированный в ресурс сокета.

Это пример HTTP-клиента на основе расширения Ev .

Расширение Ev реализует простой, но мощный универсальный цикл событий. Он не обеспечивает специфичные для сети наблюдатели, но его наблюдатель ввода / вывода может использоваться для асинхронной обработки сокетов .

В следующем коде показано, как можно запланировать HTTP-запросы для параллельной обработки.

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

тестирование

Предположим, что http://my-host.local/test.phpскрипт печатает дамп $_GET:

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

Тогда вывод php http-client.phpкоманды будет аналогичен следующему:

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

(Обрезается)

Обратите внимание, что в PHP 5, сокеты расширение может войти предупреждение о EINPROGRESS, EAGAINи EWOULDBLOCK errnoценность. Можно отключить логи с

error_reporting(E_ERROR);

По поводу "Остального" Кодекса

Я просто хочу сделать что-то подобное file_get_contents(), но не ждать завершения запроса, прежде чем выполнять остальную часть моего кода.

Код, который должен выполняться параллельно с сетевыми запросами, может быть выполнен, например, с помощью обратного вызова таймера событий или средства ожидания Ev . Вы можете легко понять это, наблюдая за примерами, упомянутыми выше. В противном случае я добавлю еще один пример :)


1

Вот рабочий пример, просто запустите его и затем откройте storage.txt, чтобы проверить волшебный результат

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

1

Вот моя собственная функция PHP, когда я делаю POST для определенного URL любой страницы .... Пример: *** использование моей функции ...

    <?php
        parse_str("email=myemail@ehehehahaha.com&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>

1

ReactPHP асинхронный http-клиент
https://github.com/shuchkin/react-http-client

Установить через Composer

$ composer require shuchkin/react-http-client

Асинхронный 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();

Запустите php в режиме CLI

$ php get.php

0

Я нахожу этот пакет довольно полезным и очень простым: 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);
}));

Он загрузит все 3 URL-адреса параллельно. Вы также можете использовать методы экземпляра класса в замыкании.

Например, я использую расширение Laravel на основе этого пакета https://github.com/spatie/laravel-collection-macros#parallelmap

Вот мой код:

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

Он загружает все необходимые данные в 10 параллельных потоков и вместо 50 секунд без асинхронной работы завершается всего за 8 секунд.


0

Symfony HttpClient является асинхронным https://symfony.com/doc/current/components/http_client.html .

Например, вы можете

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

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.