Cara membuat permintaan HTTP asinkron dalam PHP

209

Apakah ada cara di PHP untuk melakukan panggilan HTTP asinkron? Saya tidak peduli dengan jawabannya, saya hanya ingin melakukan sesuatu seperti file_get_contents(), tetapi tidak menunggu permintaan selesai sebelum mengeksekusi sisa kode saya. Ini akan sangat berguna untuk mematikan "acara" semacam di aplikasi saya, atau memicu proses panjang.

Ada ide?

Brent
sumber
9
satu fungsi - 'curl_multi', lihat di dokumen php untuknya. Haruskah memecahkan masalah Anda
James Butler
22
Judul posting ini menyesatkan. Saya datang mencari panggilan yang benar - benar tidak sinkron seperti permintaan di Node.js atau permintaan AJAX. Jawaban yang diterima bukan async (memblokir dan tidak memberikan panggilan balik), hanya permintaan sinkronisasi yang lebih cepat. Pertimbangkan untuk mengubah pertanyaan atau jawaban yang diterima.
Johntron
Bermain dengan penanganan koneksi melalui header dan buffer tidak anti peluru. Saya baru saja memposting jawaban baru yang independen dari OS, browser, atau PHP
verison
1
Asinkron bukan berarti Anda tidak peduli dengan responsnya. Itu hanya berarti panggilan tidak memblokir eksekusi utas. Asynchronous masih memerlukan respons, tetapi respons tersebut dapat diproses di utas eksekusi lainnya atau lebih baru dalam satu event loop. Pertanyaan ini menanyakan permintaan api-dan-lupa yang bisa sinkron atau asinkron tergantung pada semantik pengiriman pesan, apakah Anda peduli dengan pesanan pesan, atau konfirmasi pengiriman.
CMCDragonkai
Saya pikir Anda harus membuat permintaan HTTP api ini dalam mode non-blocking (w / c adalah apa yang Anda inginkan). Karena ketika Anda memanggil sumber daya, Anda pada dasarnya ingin tahu apakah Anda mencapai server atau tidak (atau alasan apa pun, Anda hanya perlu respons). Jawaban terbaik sebenarnya adalah fsockopen dan mengatur aliran membaca atau menulis ke mode non-blocking. Ini seperti panggilan dan lupakan.
KiX Ortillan

Jawaban:

42

Jawaban yang sebelumnya saya terima tidak berhasil. Masih menunggu tanggapan. Ini tidak bekerja meskipun, diambil dari Bagaimana cara membuat permintaan GET asynchronous di PHP?

function post_without_wait($url, $params)
{
    foreach ($params as $key => &$val) {
      if (is_array($val)) $val = implode(',', $val);
        $post_params[] = $key.'='.urlencode($val);
    }
    $post_string = implode('&', $post_params);

    $parts=parse_url($url);

    $fp = fsockopen($parts['host'],
        isset($parts['port'])?$parts['port']:80,
        $errno, $errstr, 30);

    $out = "POST ".$parts['path']." HTTP/1.1\r\n";
    $out.= "Host: ".$parts['host']."\r\n";
    $out.= "Content-Type: application/x-www-form-urlencoded\r\n";
    $out.= "Content-Length: ".strlen($post_string)."\r\n";
    $out.= "Connection: Close\r\n\r\n";
    if (isset($post_string)) $out.= $post_string;

    fwrite($fp, $out);
    fclose($fp);
}
Brent
sumber
67
Ini BUKAN async! Khususnya jika server di sisi lain turun, kode ini akan hang selama 30 detik (parameter ke-5 di fsockopen). Juga fwrite akan mengambil waktu manis untuk mengeksekusi (yang dapat Anda batasi dengan stream_set_timeout ($ fp, $ my_timeout). Yang terbaik yang dapat Anda lakukan adalah mengatur timeout rendah pada fsockopen menjadi 0,1 (100ms) dan $ my_timeout ke 100ms Anda berisiko mengambil permintaan batas waktu itu
Chris Cinelli
3
Saya yakinkan Anda bahwa itu async, dan tidak perlu waktu 30 detik. Itu batas waktu maks. Mungkin pengaturan Anda berbeda yang menyebabkan efek itu, tetapi ini bekerja dengan baik untuk saya.
Brent
11
@UlalueBrent Tidak ada dalam kode yang menunjukkan itu tidak sinkron. Itu tidak menunggu jawaban, tetapi itu tidak sinkron. Jika server jarak jauh membuka koneksi dan kemudian hang, kode ini akan menunggu selama 30 detik hingga Anda mencapai batas waktu itu.
chmac
17
alasan yang tampaknya berfungsi "async" karena Anda tidak membaca dari soket sebelum menutupnya sehingga tidak menggantung bahkan jika server tidak memancarkan respons dalam waktu. Namun ini sama sekali bukan async. Jika buffer tulis penuh (sangat kecil kemungkinannya) skrip Anda pasti akan menggantung di sana. Anda harus mempertimbangkan mengubah judul Anda menjadi sesuatu seperti "meminta halaman web tanpa menunggu tanggapan".
howanghk
3
Ini bukan async juga bukan menggunakan curl, bagaimana Anda berani menyebutnya curl_post_asyncdan mendapatkan bahkan upvotes ...
Daniel W.
27

Jika Anda mengontrol target yang ingin Anda panggil secara tidak sinkron (mis. "Longtask.php" Anda sendiri), Anda dapat menutup koneksi dari ujung itu, dan kedua skrip akan berjalan secara paralel. Ini berfungsi seperti ini:

  1. quick.php membuka longtask.php via cURL (tidak ada keajaiban di sini)
  2. longtask.php menutup koneksi dan melanjutkan (ajaib!)
  3. cURL kembali ke quick.php ketika koneksi ditutup
  4. Kedua tugas berlanjut secara paralel

Saya sudah mencoba ini, dan itu berfungsi dengan baik. Tetapi quick.php tidak akan tahu apa-apa tentang bagaimana longtask.php lakukan, kecuali jika Anda membuat beberapa alat komunikasi antara proses.

Coba kode ini di longtask.php, sebelum Anda melakukan hal lain. Ini akan menutup koneksi, tetapi masih terus berjalan (dan menekan output apa pun):

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

Kode disalin dari catatan kontribusi pengguna manual PHP dan agak ditingkatkan.

Christian Davén
sumber
3
Ini akan berhasil. Tetapi jika Anda menggunakan kerangka kerja MVC mungkin sulit untuk diimplementasikan karena cara kerangka ini mencegat dan menulis ulang panggilan. Misalnya itu tidak bekerja di Controller di CakePHP
Chris Cinelli
Keraguan tentang kode ini, proses yang harus Anda lakukan dalam longtask harus mengikuti baris ini? Terima kasih.
morgar
Itu tidak bekerja dengan sempurna. Coba tambahkan while(true);setelah kode Anda. Halaman akan menggantung, ini berarti masih berjalan di latar depan.
زياد
17

Anda dapat melakukan tipu daya dengan menggunakan exec () untuk memanggil sesuatu yang dapat melakukan permintaan HTTP, seperti wget, tetapi Anda harus mengarahkan semua output dari program ke suatu tempat, seperti file atau / dev / null, jika tidak, proses PHP akan menunggu output itu .

Jika Anda ingin memisahkan proses dari utas apache sepenuhnya, coba sesuatu seperti (Saya tidak yakin tentang ini, tapi saya harap Anda mendapatkan ide):

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

Ini bukan bisnis yang bagus, dan Anda mungkin ingin sesuatu seperti pekerjaan cron yang menggunakan skrip detak jantung yang menyurvei antrian acara basis data aktual untuk melakukan acara asinkron yang sebenarnya.

Teman Internet
sumber
3
Demikian pula, saya juga telah melakukan yang berikut: exec ("curl $ url> / dev / null &");
Matt Huggins
2
Pertanyaan: adakah manfaat memanggil 'bash -c "wget"' daripada hanya 'wget'?
Matt Huggins
2
Dalam pengujian saya, menggunakan exec("curl $url > /dev/null 2>&1 &");adalah salah satu solusi tercepat di sini. Ini jauh lebih cepat (1,9 detik untuk 100 iterasi) daripada post_without_wait()fungsi (14,8 detik) dalam jawaban "diterima" di atas. DAN itu satu
kalimat
Gunakan path lengkap (mis. / Usr / bin / curl) untuk membuatnya lebih cepat
Putnik
apakah ini menunggu hingga skrip selesai?
cikatomo
11

Pada 2018, Guzzle telah menjadi perpustakaan standar de facto untuk permintaan HTTP, yang digunakan dalam beberapa kerangka kerja modern. Ini ditulis dalam PHP murni dan tidak perlu menginstal ekstensi khusus.

Ia dapat melakukan panggilan HTTP asinkron dengan sangat baik, dan bahkan menggabungkannya seperti ketika Anda perlu melakukan 100 panggilan HTTP, tetapi tidak ingin menjalankan lebih dari 5 sekaligus.

Contoh permintaan bersamaan

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]

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

Simon Timur
sumber
3
Namun, jawaban ini tidak sinkron. rupanya guzzle tidak melakukan itu
daslicious
2
Guzzle mengharuskan Anda menginstal curl. Kalau tidak, itu tidak paralel, dan tidak memberi Anda peringatan bahwa itu tidak paralel.
Velizar Hristov
Terima kasih atas tautan @daslicious - ya, tampaknya itu tidak sepenuhnya async (seperti ketika Anda ingin mengirim permintaan tetapi tidak peduli dengan hasilnya) tetapi beberapa posting di utas yang pengguna telah menawarkan solusi oleh menetapkan nilai batas waktu permintaan yang sangat rendah yang masih mengizinkan waktu koneksi, tetapi tidak menunggu hasilnya.
Simon East
9
/**
 * Asynchronously execute/include a PHP file. Does not record the output of the file anywhere. 
 *
 * @param string $filename              file to execute, relative to calling script
 * @param string $options               (optional) arguments to pass to file via the command line
 */ 
function asyncInclude($filename, $options = '') {
    exec("/path/to/php -f {$filename} {$options} >> /dev/null &");
}
philfreo
sumber
Ini bukan asinkron karena exec memblokir sampai Anda berhenti atau melakukan proses yang ingin Anda jalankan.
Daniel W.
6
Apakah Anda memperhatikan &di bagian akhir?
philfreo
Jadi apakah ini akan memblokir skrip atau tidak, saya bingung?
pleshy
1
@pleshy itu tidak akan. ampersand (&) artinya menjalankan skrip di latar belakang
daisura99
8

Anda dapat menggunakan perpustakaan ini: https://github.com/stil/curl-easy

Maka cukup mudah:

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

Di bawah ini Anda dapat melihat keluaran konsol dari contoh di atas. Ini akan menampilkan jam langsung sederhana yang menunjukkan berapa banyak permintaan waktu berjalan:


animasi

stil
sumber
Ini harus menjadi jawaban yang diterima untuk pertanyaan itu karena, bahkan jika itu tidak benar async, itu lebih baik daripada yang diterima dan semua jawaban "async" dengan guzzle (Di sini Anda dapat melakukan operasi saat permintaan dilakukan)
0ddlyoko
7
  1. Palsu aborsi permintaan menggunakan CURLpengaturan rendahCURLOPT_TIMEOUT_MS

  2. atur ignore_user_abort(true)untuk tetap memproses setelah koneksi ditutup.

Dengan metode ini tidak perlu menerapkan penanganan koneksi melalui header dan buffer terlalu tergantung pada versi OS, Browser dan PHP

Proses master

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

Proses latar belakang

ignore_user_abort(true);

//do something...

NB

Jika Anda ingin cURL timeout dalam waktu kurang dari satu detik, Anda dapat menggunakan CURLOPT_TIMEOUT_MS, meskipun ada bug / "fitur" pada "sistem mirip Unix" yang menyebabkan libcurl langsung kehabisan waktu jika nilainya <1000 ms dengan kesalahan " Kesalahan CURL (28): Batas waktu tercapai ". Penjelasan untuk perilaku ini adalah:

[...]

Solusinya adalah menonaktifkan sinyal menggunakan CURLOPT_NOSIGNAL

Sumber daya

RafaSashi
sumber
Bagaimana Anda menangani waktu habis koneksi (menyelesaikan, dns)? Ketika saya mengatur timeout_ms ke 1, saya selalu berakhir dengan "menyelesaikan batas waktu setelah 4 ms" atau sesuatu seperti itu
Martin Wickman
Saya tidak tahu tetapi 4 ms suara sudah cukup cepat bagi saya ... Saya tidak berpikir Anda dapat menyelesaikan lebih cepat dengan mengubah pengaturan ikal. Coba optimalkan permintaan yang ditargetkan mungkin ...
RafaSashi
Oke, tetapi timeout_ms = 1 menetapkan batas waktu untuk seluruh permintaan. Jadi, jika tekad Anda membutuhkan lebih dari 1 ms, maka ikal akan kehabisan waktu dan menghentikan permintaan. Saya tidak melihat bagaimana ini bisa bekerja sama sekali (dengan asumsi resolusi membutuhkan> 1 ms).
Martin Wickman
4

biarkan saya menunjukkan jalan saya :)

membutuhkan nodejs yang terinstal di server

(server saya mengirim 1000 https, dapatkan permintaan hanya membutuhkan waktu 2 detik)

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);
}
pengguna1031143
sumber
1
Harap dicatat bahwa banyak penyedia hosting tidak mengizinkan penggunaan fungsi PHP tertentu (seperti popen / exec ). Lihat menonaktifkan_fungsi direktif PHP.
Eugen Mihailescu
4

Ekstensi swoole. https://github.com/matyhtf/swoole Asynchronous & concurrent framework jaringan untuk PHP.

$client = new swoole_client(SWOOLE_SOCK_TCP, SWOOLE_SOCK_ASYNC);

$client->on("connect", function($cli) {
    $cli->send("hello world\n");
});

$client->on("receive", function($cli, $data){
    echo "Receive: $data\n";
});

$client->on("error", function($cli){
    echo "connect fail\n";
});

$client->on("close", function($cli){
    echo "close\n";
});

$client->connect('127.0.0.1', 9501, 0.5);
Tony
sumber
4

Anda dapat menggunakan soket yang tidak menghalangi dan salah satu ekstensi pecl untuk PHP:

Anda dapat menggunakan pustaka yang memberi Anda lapisan abstraksi antara kode Anda dan ekstensi pecl: https://github.com/reactphp/event-loop

Anda juga dapat menggunakan async http-client, berdasarkan pada pustaka sebelumnya: https://github.com/reactphp/http-client

Lihat perpustakaan ReactPHP lainnya: http://reactphp.org

Hati-hati dengan model asinkron. Saya merekomendasikan untuk melihat video ini di youtube: http://www.youtube.com/watch?v=MWNcItWuKpI

Roman Shamritskiy
sumber
3
class async_file_get_contents extends Thread{
    public $ret;
    public $url;
    public $finished;
        public function __construct($url) {
        $this->finished=false;
        $this->url=$url;
    }
        public function run() {
        $this->ret=file_get_contents($this->url);
        $this->finished=true;
    }
}
$afgc=new async_file_get_contents("http://example.org/file.ext");
hanshenrik
sumber
2

Perpanjangan Acara

Perpanjangan acara sangat tepat. Ini adalah port dari perpustakaan Libevent yang dirancang untuk I / O yang digerakkan oleh acara, terutama untuk jaringan.

Saya telah menulis sampel klien HTTP yang memungkinkan untuk menjadwalkan sejumlah permintaan HTTP dan menjalankannya secara tidak sinkron.

Ini adalah contoh kelas klien HTTP berdasarkan ekstensi Acara .

Kelas memungkinkan untuk menjadwalkan sejumlah permintaan HTTP, kemudian menjalankannya secara tidak sinkron.

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

Ini adalah contoh skrip di sisi server.

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

Pemakaian

php http-client.php

Output Sampel

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

(Dipangkas.)

Catatan, kode ini dirancang untuk pemrosesan jangka panjang dalam CLI SAPI .


Untuk protokol khusus, pertimbangkan untuk menggunakan API tingkat rendah, yaitu acara buffer , buffer . Untuk komunikasi SSL / TLS, saya akan merekomendasikan API tingkat rendah bersamaan dengan konteks ssl Acara . Contoh:


Meskipun Libevent's HTTP API sederhana, itu tidak fleksibel seperti acara penyangga. Misalnya, API HTTP saat ini tidak mendukung metode HTTP khusus. Tetapi dimungkinkan untuk mengimplementasikan hampir semua protokol menggunakan API tingkat rendah.

Perpanjangan Ev

Saya juga telah menulis sampel klien HTTP lain menggunakan ekstensi Ev dengan soket dalam mode non-blocking . Kode sedikit lebih verbose daripada sampel berdasarkan pada Peristiwa, karena Ev adalah perulangan peristiwa tujuan umum. Itu tidak menyediakan fungsi-fungsi khusus jaringan, tetapi EvIopengamatnya mampu mendengarkan deskriptor file yang dienkapsulasi ke sumber daya soket, khususnya.

Ini adalah contoh klien HTTP berdasarkan ekstensi Ev .

Ekstensi Ev mengimplementasikan loop acara tujuan umum sederhana namun kuat. Itu tidak menyediakan pengamat spesifik jaringan, tetapi pengamat I / O-nya dapat digunakan untuk pemrosesan soket yang tidak sinkron .

Kode berikut menunjukkan bagaimana permintaan HTTP dapat dijadwalkan untuk pemrosesan paralel.

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

Pengujian

Misalkan http://my-host.local/test.phpskrip sedang mencetak dump $_GET:

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

Maka output dari php http-client.phpperintah akan serupa dengan yang berikut:

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

(dipangkas)

Catatan, di PHP 5 yang soket ekstensi dapat log peringatan untuk EINPROGRESS, EAGAINdan EWOULDBLOCK errnonilai-nilai. Dimungkinkan untuk mematikan log dengan

error_reporting(E_ERROR);

Tentang "Sisa" Kode

Saya hanya ingin melakukan sesuatu seperti file_get_contents(), tetapi tidak menunggu permintaan selesai sebelum mengeksekusi sisa kode saya.

Kode yang seharusnya berjalan secara paralel dengan permintaan jaringan dapat dieksekusi dalam panggilan balik dari pengatur waktu Acara , atau pengamat siaga Ev , misalnya. Anda dapat dengan mudah mengetahuinya dengan menonton sampel yang disebutkan di atas. Kalau tidak, saya akan menambahkan contoh lain :)

Ruslan Osmanov
sumber
1

Ini adalah contoh yang berfungsi, jalankan saja dan buka storage.txt sesudahnya, untuk memeriksa hasil ajaibnya

<?php
    function curlGet($target){
        $ch = curl_init();
        curl_setopt($ch, CURLOPT_URL, $target);
        curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
        $result = curl_exec ($ch);
        curl_close ($ch);
        return $result;
    }

    // Its the next 3 lines that do the magic
    ignore_user_abort(true);
    header("Connection: close"); header("Content-Length: 0");
    echo str_repeat("s", 100000); flush();

    $i = $_GET['i'];
    if(!is_numeric($i)) $i = 1;
    if($i > 4) exit;
    if($i == 1) file_put_contents('storage.txt', '');

    file_put_contents('storage.txt', file_get_contents('storage.txt') . time() . "\n");

    sleep(5);
    curlGet($_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?i=' . ($i + 1));
    curlGet($_SERVER['HTTP_HOST'] . $_SERVER['SCRIPT_NAME'] . '?i=' . ($i + 1));
AlexTR
sumber
1

Ini adalah fungsi PHP saya sendiri ketika saya melakukan POST ke URL spesifik halaman mana pun .... Contoh: *** penggunaan Fungsi saya ...

    <?php
        parse_str("[email protected]&subject=this is just a test");
        $_POST['email']=$email;
        $_POST['subject']=$subject;
        echo HTTP_POST("http://example.com/mail.php",$_POST);***

    exit;
    ?>
    <?php
    /*********HTTP POST using FSOCKOPEN **************/
    // by ArbZ

function HTTP_Post($URL,$data, $referrer="") {

    // parsing the given URL
    $URL_Info=parse_url($URL);

    // Building referrer
    if($referrer=="") // if not given use this script as referrer
        $referrer=$_SERVER["SCRIPT_URI"];

    // making string from $data
    foreach($data as $key=>$value)
        $values[]="$key=".urlencode($value);
        $data_string=implode("&",$values);

    // Find out which port is needed - if not given use standard (=80)
    if(!isset($URL_Info["port"]))
        $URL_Info["port"]=80;

    // building POST-request: HTTP_HEADERs
    $request.="POST ".$URL_Info["path"]." HTTP/1.1\n";
    $request.="Host: ".$URL_Info["host"]."\n";
    $request.="Referer: $referer\n";
    $request.="Content-type: application/x-www-form-urlencoded\n";
    $request.="Content-length: ".strlen($data_string)."\n";
    $request.="Connection: close\n";
    $request.="\n";
    $request.=$data_string."\n";

    $fp = fsockopen($URL_Info["host"],$URL_Info["port"]);
    fputs($fp, $request);
    while(!feof($fp)) {
        $result .= fgets($fp, 128);
    }
    fclose($fp); //$eco = nl2br();


    function getTextBetweenTags($string, $tagname) {
        $pattern = "/<$tagname ?.*>(.*)<\/$tagname>/";
        preg_match($pattern, $string, $matches);
        return $matches[1];
    }
    //STORE THE FETCHED CONTENTS to a VARIABLE, because its way better and fast...
    $str = $result;
    $txt = getTextBetweenTags($str, "span"); $eco = $txt;  $result = explode("&",$result);
    return $result[1];
    <span style=background-color:LightYellow;color:blue>".trim($_GET['em'])."</span>
    </pre> "; 
}
</pre>
saya ArbZ
sumber
1

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

Instal melalui Komposer

$ composer require shuchkin/react-http-client

Async HTTP DAPATKAN

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

Jalankan php dalam mode CLI

$ php get.php
Sergey Shuchkin
sumber
0

Saya menemukan paket ini sangat berguna dan sangat sederhana: 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);
}));

Ini akan memuat semua 3 url secara paralel. Anda juga dapat menggunakan metode instance kelas di penutupan.

Misalnya saya menggunakan ekstensi Laravel berdasarkan paket ini https://github.com/spatie/laravel-collection-macros#parallelmap

Ini kode saya:

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

Itu memuat semua data yang diperlukan dalam 10 utas paralel dan bukannya 50 detik tanpa async selesai hanya dalam 8 detik.

Vedmant
sumber
0

Symfony HttpClient adalah asinkron https://symfony.com/doc/current/components/http_client.html .

Misalnya kamu bisa

use Symfony\Component\HttpClient\HttpClient;

$client = HttpClient::create();
$response1 = $client->request('GET', 'https://website1');
$response2 = $client->request('GET', 'https://website1');
$response3 = $client->request('GET', 'https://website1');
//these 3 calls with return immediately
//but the requests will fire to the website1 webserver

$response1->getContent(); //this will block until content is fetched
$response2->getContent(); //same 
$response3->getContent(); //same
nacholibre
sumber
-4

Nah, batas waktu dapat diatur dalam milidetik, lihat "CURLOPT_CONNECTTIMEOUT_MS" di http://www.php.net/manual/en/function.curl-setopt

Akhil Sikri
sumber
3
Itu hanya membuat topi berpikir batas waktu. Sama sekali tidak async.
Chris Cinelli