Як робити асинхронні запити HTTP в PHP


209

Чи є спосіб у PHP здійснювати асинхронні дзвінки HTTP? Мене не хвилює відповідь, я просто хочу зробити щось на кшталт file_get_contents(), але не чекати, коли запит закінчиться, перш ніж виконати решту мого коду. Це було б дуже корисно для встановлення подібних "подій" у моїй програмі або запуску тривалих процесів.

Будь-які ідеї?


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

22
Назва цієї публікації вводить в оману. Я прийшов шукати справді асинхронні дзвінки, схожі на запити в Node.js або запит AJAX. Прийнята відповідь не є асинхронною (блокує та не забезпечує зворотний дзвінок), а лише швидший синхронний запит. Подумайте про зміну питання чи прийняту відповідь.
Джоннтрон

Гра з керуванням підключенням через заголовки та буфер не захищена від куль. Я щойно опублікував нову відповідь, незалежну від ОС, браузера або PHP
verison

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
причина, що, здається, працює "асинхронізувати", тому що ви не читаєте з сокета перед тим, як закрити його, щоб він не завис, навіть якщо сервер не видав відповідь вчасно. Однак це абсолютно не асинхронність. Якщо буфер запису заповнений (найменш вірогідний), ваш сценарій обов'язково там буде висіти. Слід розглянути можливість змінити назву на щось на кшталт "запит на веб-сторінку, не чекаючи відповіді".
хоангік

3
Це не асинхронізація, і це не використання curl, як ви наважуєтесь називати це curl_post_asyncта отримувати рівне оновлення ...
Daniel W.

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
Chris Cinelli

Сумніви щодо цього коду, процес, який вам потрібно зробити в longtask, повинен тривати після цих рядків? Дякую.
моргар

Це не працює ідеально. Спробуйте додати while(true);після коду. Сторінка буде висіти, це означає, що вона все ще працює на передньому плані.
زياد

17

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

Якщо ви хочете повністю відокремити процес від потоку apache, спробуйте щось на кшталт (я не впевнений у цьому, але сподіваюся, що ви зрозумієте цю ідею):

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

Це не приємний бізнес, і ви, мабуть, захочете щось подібне до роботи із крон, викликаючи сценарій серцебиття, який опитує фактичну чергу подій бази даних, щоб робити справжні асинхронні події.


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 / bin / curl), щоб зробити його ще швидшим
Putnik

це чекає, поки сценарій буде закінчений?
cikatomo

11

Станом на 2018 рік Guzzle стала стандартною бібліотекою дефакто для запитів 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
Однак ця відповідь не є асинхронною. мабуть,
загадка

2
Guzzle вимагає, щоб ви встановили curl. В іншому випадку він не паралельний, і це не дає вам попередження про те, що це не паралельно.
Велізар Христов

Дякуємо за посилання @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
Ви помітили &це наприкінці?
філфрео

Так би блокував сценарій тоді чи ні, я їх плутав?
плеші

1
@pleshy це не буде. ampersand (&) означає запустити сценарій у фоновому режимі
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

Ресурси


Як ви обробляєте час очікування з'єднання (вирішення, dns)? Коли я встановлюю timeout_ms на 1, я завжди закінчуюсь "вирішенням часу після 4 мс" або щось подібне
Мартін Вікман

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

Гаразд, але timeout_ms = 1 встановлює час очікування для всього запиту. Отже, якщо ваша рішучість займає більше 1 мс, згортання зупинить та припинить запит. Я не бачу, як це може взагалі працювати (якщо вважати, що дозвіл займає більше 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.
Євген Михайлеску

4

Розширення шпули. 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-клієнт async на основі попередньої бібліотеки: 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

Розширення події

Розширення подій дуже доречно. Це порт Лівенвента бібліотеки який призначений для введення-виводу, керованого подіями, головним чином для мереж.

Я написав зразок HTTP-клієнта, який дозволяє планувати ряд HTTP-запитів і запускати їх асинхронно.

Це зразок класу клієнтів HTTP на основі подій розширення .

Клас дозволяє запланувати ряд 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 події . Приклади:


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

Розширення Ev

Я також написав зразок іншого клієнта HTTP, що використовує розширення Ev з сокетами в режимі, що не блокує . Код дещо докладніший, ніж зразок на основі події, оскільки 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 async http-клієнт
https://github.com/shuchkin/react-http-client

Встановити через 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();

Запустіть 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.