PHP에서 비동기 HTTP 요청을 만드는 방법


209

PHP에서 비동기 HTTP 호출을 수행하는 방법이 있습니까? 응답에 신경 쓰지 않고 단지 다음과 같은 일을하고 싶습니다.file_get_contents() 하고 싶지만 나머지 코드를 실행하기 전에 요청이 끝날 때까지 기다리지 않습니다. 이것은 내 응용 프로그램에서 일종의 "이벤트"를 설정하거나 긴 프로세스를 트리거하는 데 매우 유용합니다.

어떤 아이디어?


9
하나의 기능- 'curl_multi', PHP 문서를보십시오. 문제를 해결해야합니다
James Butler

22
이 게시물의 제목이 잘못되었습니다. Node.js의 요청 또는 AJAX 요청과 유사한 진정한 비동기 호출을 찾고있었습니다 . 수락 된 답변은 비동기 적이 지 않으며 (콜백을 차단하고 제공하지 않음) 더 빠른 동기 요청입니다. 질문 또는 수락 된 답변을 변경하십시오.
Johntron

헤더와 버퍼를 통한 연결 처리는 방탄이 아닙니다. 방금 OS, 브라우저 또는 PHP 버전에서 독립된 새로운 답변을 게시했습니다
RafaSashi

1
비동기는 응답에 신경 쓰지 않는다는 것을 의미하지 않습니다. 그것은 호출이 메인 스레드 실행을 차단하지 않는다는 것을 의미합니다. 비동기에는 여전히 응답이 필요하지만 다른 실행 스레드에서 또는 나중에 이벤트 루프에서 응답을 처리 할 수 ​​있습니다. 이 질문은 메시지 순서에 상관없이 메시지 배달 또는 배달 확인에 관계없이 메시지 배달 의미에 따라 동기식 또는 비동기식 일 수있는 실행 요청을 요청합니다.
CMCDragonkai

비 차단 모드에서이 fire HTTP 요청을해야한다고 생각합니다 (w / c는 실제로 원하는 것입니다.). 응답이 필요합니다). 가장 좋은 대답은 실제로 fsockopen이며 스트림 읽기 또는 쓰기를 비 블로킹 모드로 설정하는 것입니다. 그것은 전화하고 잊는 것과 같습니다.
KiX Ortillan

답변:


42

이전에 수락 한 답변이 작동하지 않았습니다. 여전히 응답을 기다렸습니다. 이것은 PHP에서 비동기 GET 요청어떻게합니까? 에서 가져온 것입니다 .

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 초 동안 (fsockopen의 5 번째 매개 변수) 정지됩니다. 또한 fwrite는 실행하는 데 달콤한 시간이 걸릴 것입니다 (stream_set_timeout ($ fp, $ my_timeout)으로 제한 할 수 있습니다. fsockopen에서 낮은 시간 초과를 0.1 (100ms)로, $ my_timeout을 100ms로 설정하는 것이 가장 좋습니다. 그러나 요청 시간이 초과 될 위험이 있습니다.
Chris Cinelli

3
나는 그것이 비동기이며 30 초가 걸리지 않는다는 것을 확신합니다. 최대 시간 제한입니다. 설정이 다르면 효과가 발생하는 것이 가능하지만 이것은 나에게 효과적이었습니다.
브렌트

11
@UltimateBrent 코드에 비동기임을 제안하는 것은 없습니다. 응답을 기다리지 않지만 비동기식은 아닙니다. 원격 서버가 연결을 연 다음 중단되면이 코드는 해당 시간 초과에 도달 할 때까지 30 초 동안 기다립니다.
chmac

17
소켓을 닫기 전에 읽지 않았기 때문에 "비동기"로 작동하는 이유는 서버가 시간 내에 응답을 보내지 않아도 중단되지 않았기 때문입니다. 그러나 이것은 절대적으로 비동기가 아닙니다. 쓰기 버퍼가 가득 차면 (최소한 가능성) 스크립트가 확실히 중단됩니다. 제목을 "응답을 기다리지 않고 웹 페이지 요청"과 같은 제목으로 변경하는 것을 고려해야합니다.
howanghk

3
이것은 비동 시적이거나 컬을 사용하지 않으며, 당신이 그것을 감히 부르고 curl_post_async공평하게하는 방법 ...
Daniel W.

27

비동기식으로 호출하려는 대상 (예 : 자신의 "longtask.php")을 제어하는 ​​경우 해당 끝에서 연결을 닫을 수 있으며 두 스크립트가 동시에 실행됩니다. 다음과 같이 작동합니다.

  1. quick.php는 cURL을 통해 longtask.php를 엽니 다 (여기에는 마법이 없습니다).
  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의의 컨트롤러에서 작동하지 않습니다
크리스 치 넬리

이 코드에 대한 의문, 긴 작업에서 수행 해야하는 프로세스는이 줄을 따라야합니까? 감사.
morgar

완벽하게 작동하지 않습니다. while(true);코드 뒤에 추가 하십시오. 페이지가 중단됩니다. 즉, 여전히 전경에서 실행 중입니다.
زياد

17

exec ()를 사용하여 HTTP 요청을 할 수있는 것을 호출하여 속임수를 쓸 수 wget있지만, 프로그램의 모든 출력을 파일이나 / dev / null과 같은 다른 곳으로 보내야합니다. 그렇지 않으면 PHP 프로세스가 해당 출력을 기다립니다 .

프로세스를 아파치 스레드와 완전히 분리하려면 다음과 같이 시도하십시오 (나는 확실하지 않지만 아이디어를 얻길 바랍니다).

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

그것은 좋은 사업이 아니며 아마도 실제 데이터베이스 이벤트 큐를 폴링하여 실제 비동기 이벤트를 수행하는 하트 비트 스크립트를 호출하는 크론 작업과 같은 것을 원할 것입니다.


3
마찬가지로 다음 작업도 수행했습니다. exec ( "curl $ url> / dev / null &");
Matt Huggins

2
질문 : 'wget'이 아닌 'bash -c "wget"'을 호출하면 이점이 있습니까?
Matt Huggins

2
필자의 테스트에서 사용 exec("curl $url > /dev/null 2>&1 &");은 여기에서 가장 빠른 솔루션 중 하나입니다. post_without_wait()위의 "허용 된"답변 의 함수 (14.8 초) 보다 훨씬 빠릅니다 (100 회 반복시 1.9 초). 그리고 그것은 하나의 라이너입니다 ...
rinogo

전체 경로 (예 : / 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
그러나 이것은 비동기식이 아닙니다. 분명히 그렇게하지 않는 폭식
daslicious

2
Guzzle은 curl을 설치해야합니다. 그렇지 않으면 병렬이 아니며 병렬이 아니라는 경고를 표시하지 않습니다.
Velizar Hristov

링크 @daslicious에 감사드립니다-예, 그것은 완전히 비동기 적이 지 않은 것으로 보입니다 (요청을 보내려고하지만 결과에 신경 쓰지 않을 때와 같이). 그 스레드에서 사용자가 해결 한 방법 여전히 연결 시간을 허용하지만 결과를 기다리지 않는 요청 시간 초과 값을 매우 낮게 설정합니다.
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 &");
}

실행하려는 프로세스를 종료하거나 분기 할 때까지 exec가 차단되기 때문에 이것은 비동기 적이 지 않습니다.
Daniel W.

6
&마지막에 눈치 채 셨나요 ?
philfreo

그러면 스크립트가 차단 될까요? 혼란 스럽습니까?
pleshy

1
@pleshy하지 않습니다. 앰퍼샌드 (&)는 백그라운드에서 스크립트를 실행하는 것을 의미합니다.
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
}

아래는 위 예제의 콘솔 출력입니다. 얼마나 많은 시간 요청이 실행되고 있는지를 나타내는 간단한 라이브 시계가 표시됩니다.


생기


이 질문에 대한 정답은 사실입니다. 비록 그것이 사실 비동기가 아니더라도, 답안 된 모든 답변과 guzzle을 사용한 모든 "비동기"답변보다 낫기 때문입니다 (요청이 수행되는 동안 작업을 수행 할 수 있습니다)
0ddlyoko

7
  1. CURL낮음 설정을 사용하여 요청 중단을 가짜CURLOPT_TIMEOUT_MS

  2. ignore_user_abort(true)연결이 닫힌 후에도 처리를 유지하도록 설정 합니다.

이 방법을 사용하면 OS, 브라우저 및 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이 1 초 이내에 시간 초과되도록하려면 CURLOPT_TIMEOUT_MS를 사용할 수 있지만 "Unix-like systems"에 버그 / "기능"이있어 값이 <1000ms 인 경우 libcurl이 즉시 시간 초과되도록하는 버그 / "기능"이 있습니다. cURL 오류 (28) : 시간이 초과되었습니다 ". 이 동작에 대한 설명은 다음과 같습니다.

[...]

해결책은 CURLOPT_NOSIGNAL을 사용하여 신호를 비활성화하는 것입니다.

자원


연결 시간 초과를 어떻게 처리합니까 (dns, dns)? timeout_ms를 1로 설정하면 항상 "4ms 후에 시간 초과 해결"또는 이와 유사한 결과가 나타납니다.
Martin Wickman

잘 모르겠지만 4ms 소리가 이미 꽤 빠릅니다 ... 컬 설정을 변경하여 더 빨리 해결할 수 있다고 생각하지 않습니다. 대상 요청을 최적화 해보십시오.
RafaSashi

좋습니다. 그러나 timeout_ms = 1은 전체 요청에 대한 시간 초과를 설정합니다. 따라서 해결에 1ms 이상이 걸리면 curl이 시간 초과되고 요청이 중지됩니다. 이것이 어떻게 작동하는지 알 수 없습니다 (분해능이 1ms 이상이라고 가정).
Martin Wickman

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
많은 호스팅 제공 업체는 popen / exec 와 같은 특정 PHP 기능의 사용을 허용하지 않습니다 . disable_functions PHP 지시문을 참조하십시오.
Eugen Mihailescu

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

PHP에 비 차단 소켓과 pecl 확장 중 하나를 사용할 수 있습니다.

코드와 pecl 확장자 사이에 추상화 계층을 제공하는 라이브러리를 사용할 수 있습니다 : https://github.com/reactphp/event-loop

이전 라이브러리를 기반으로 비동기 http-client를 사용할 수도 있습니다. 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

이벤트 확장

이벤트 확장이 매우 적합합니다. 주로 네트워킹을 위해 이벤트 중심 I / O를 위해 설계된 Libevent 라이브러리 의 포트입니다 .

많은 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 통신의 경우 Event ssl context 와 함께 저수준 API를 권장합니다 . 예 :


Libevent의 HTTP API는 단순하지만 버퍼 이벤트만큼 유연하지 않습니다. 예를 들어, HTTP API는 현재 사용자 정의 HTTP 메소드를 지원하지 않습니다. 그러나 저수준 API를 사용하여 거의 모든 프로토콜을 구현할 수 있습니다.

Ev 확장

비 차단 모드 에서 소켓이 있는 Ev 확장을 사용하는 다른 HTTP 클라이언트 샘플도 작성했습니다 . Ev는 범용 이벤트 루프이기 때문에 코드는 Event 기반 샘플보다 약간 더 장황합니다. 네트워크 특정 기능을 제공하지는 않지만 감시자는 특히 소켓 리소스에 캡슐화 된 파일 디스크립터를들을 수 있습니다.EvIo

Ev 확장에 기반한 샘플 HTTP 클라이언트입니다 .

Ev 확장은 단순하면서도 강력한 범용 이벤트 루프를 구현합니다. 네트워크 특정 감시자를 제공하지는 않지만 I / O 감시자소켓의 비동기 처리에 사용될 수 있습니다 .

다음 코드는 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, EAGAINEWOULDBLOCK 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

다음은 페이지의 특정 URL에 POST를 수행 할 때의 고유 한 PHP 함수입니다.

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

CLI 모드에서 PHP 실행

$ 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을 모두 동시에로드합니다. 클로저에서 클래스 인스턴스 메서드를 사용할 수도 있습니다.

예를 들어이 패키지 https://github.com/spatie/laravel-collection-macros#parallelmap을 기반으로 한 Laravel 확장을 사용합니다

내 코드는 다음과 같습니다.

    /**
     * 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

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.