如何在PHP中发起异步GET请求?

104

我希望向不同服务器上的另一个脚本发送简单GET请求。我该怎么做?

在某些情况下,我只需要请求外部脚本而无需任何输出。

make_request('http://www.externalsite.com/script1.php?variable=45'); //example usage
在第二种情况下,我需要获取文本输出。
$output = make_request('http://www.externalsite.com/script2.php?variable=45');
echo $output; //string output
说实话,我不想用CURL来搞事情,因为这不是CURL的真正工作。我也不想使用http_get,因为我没有安装PECL扩展。fsockopen有用吗?如果有,我该如何做到这一点,而不需要读取文件的内容?难道没有其他方法吗?谢谢大家。
更新:我应该补充说明,在第一种情况下,我不想等待脚本返回任何内容。据我所知,file_get_contents()会等待页面完全加载等等?

7
@William:是的,大多数问题都可以被视为它们自身的精确重复。😎我认为您发布了错误的链接... - RichieHindle
1
我本意是要发布musicfreak发布的链接,弄混了我的标签页;-) - William Brendel
2
@Richie: “大多数”问题? ;) - Sasha Chedygov
1
我重新命名了这个问题,以使其与其他问题区分开来,因为似乎您想发出请求,而不关心使用响应(因此它可以在脚本的其余部分运行时发生)。如果我错了,请恢复它! - dbr
为什么不直接使用exec调用另一个PHP脚本,然后在那个脚本中使用file_get_content呢? - memical
显示剩余5条评论
22个回答

49

file_get_contents可以满足你的需求。

$output = file_get_contents('http://www.example.com/');
echo $output;

编辑:一种触发GET请求并立即返回的方法。

引用自http://petewarden.typepad.com/searchbrowser/2008/06/how-to-post-an.html

function curl_post_async($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);
}

这段代码打开一个套接字,发送一个GET请求,然后立即关闭套接字并返回。


6
curl_post_async 发送的是一个 POST 请求,而不是 GET 请求。 - Vinko Vrsalovic
13
我说这个函数的命名不太合适,是不是对的?它实际上与curl库没有任何关系。更像是fsock_post_async()。 - MikeMurko
67
这不是异步的!特别是如果另一端的服务器宕机,这段代码将会挂起30秒(fsockopen中的第5个参数)。此外,fwrite将需要很长时间才能执行(您可以通过stream_set_timeout($fp, $my_timeout)来限制时间)。最好的做法是在fsockopen上设置较低的超时时间为0.1秒(100毫秒),$my_timeout也设置为100毫秒。但是请注意,这样有可能会导致请求超时。 - Chris Cinelli
4
这与异步无关,这就是同步操作... 异步的意思是在执行这项任务时做其他任务。这是并行执行。 - CodeAngry
20
这既不是异步操作,也没有使用curl库,你怎么敢称它为 curl_post_async,还得到了赞同... - Daniel W.
显示剩余11条评论

32

这是如何使Marquis的答案适用于POST和GET请求:

  // $type must equal 'GET' or 'POST'
  function curl_request_async($url, $params, $type='POST')
  {
      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);

      // Data goes in the path for a GET request
      if('GET' == $type) $parts['path'] .= '?'.$post_string;

      $out = "$type ".$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";
      // Data goes in the request body for a POST request
      if ('POST' == $type && isset($post_string)) $out.= $post_string;

      fwrite($fp, $out);
      fclose($fp);
  }

2
这是一个方便的代码片段,我一直在这里和那里使用它,但现在我发现我需要做同样的事情,但是使用SSL站点。除了HTTP/1.1类型和端口之外,我需要改变什么吗? - Kevin Jhangiani
2
回答有关使用此项来进行 SSL 的问题,您可以通过将端口更改为 443 并在 fsockopen 中的端口名称后附加 ssl:// 来使其支持 SSL:$fp = fsockopen("ssl://".$parts['host'],… - Michael Dogger
1
除了HTTP/1.1类型和端口之外,我需要更改的内容还有吗?- 是的,您应该使用主机名作为ssl://hostname而不是仅使用hostname来调用fsockopen()。 - Cowlby
24
这不是异步的!特别是如果另一端的服务器关闭,这段代码将会阻塞30秒钟(即fsockopen中的第5个参数)。此外,fwrite也需要一些时间来执行(可以使用stream_set_timeout($fp, $my_timeout)进行限制)。最好的选择是将fsockopen的超时时间设置为0.1(100毫秒),并将$my_timeout设置为100毫秒。但你可能会冒着请求超时的风险。 - Chris Cinelli
1
Content-Length 不应该在 GET 请求中设置。在某些情况下可能不会导致错误,但在我的情况下,这导致调用的 PHP 脚本无法处理请求。 - user3285954
显示剩余2条评论

15
关于您的更新,关于不想等待完整页面加载的问题 - 我认为您需要的是一个HTTP HEAD 请求。

get_headers 可以做到这一点 - 我认为它只请求头部信息,因此不会发送完整的页面内容。

"PHP / Curl: HEAD Request takes a long time on some sites" 描述了如何使用PHP/Curl进行HEAD 请求。

如果您想触发请求,并且不会完全阻塞脚本,有几种不同复杂程度的方法可以实现。

  • 将HTTP请求作为后台进程执行,php执行后台进程 - 基本上你会执行类似于"wget -O /dev/null $carefully_escaped_url"这样的命令 - 这将是特定于平台的,而且你必须对命令中的参数进行非常小心的转义
  • 在后台执行PHP脚本 - 基本上与UNIX进程方法相同,但执行的是PHP脚本而不是shell命令
  • 使用数据库(或类似于beanstalkd这样的工具,可能过于复杂)来创建一个"作业队列"。你将URL添加到队列中,然后后台进程或cron作业定期检查新的作业并对URL执行请求

+1 对于我之前没有想到的各种有趣选项。 - Jasdeep Khalsa
“我认为它只请求头部信息” - 也许是这样,但是没有什么可以阻止文档在响应HEAD请求时发送完整的响应正文。我猜测这种方法会在底层使用fsock,并强制等待(并读取)完整的响应。 - hiburn8

6
我建议您使用经过充分测试的 PHP 库:curl-easy
<?php
$request = new cURL\Request('http://www.externalsite.com/script2.php?variable=45');
$request->getOptions()
    ->set(CURLOPT_TIMEOUT, 5)
    ->set(CURLOPT_RETURNTRANSFER, true);

// add callback when the request will be completed
$request->addListener('complete', function (cURL\Event $event) {
    $response = $event->response;
    $content = $response->getContent();
    echo $content;
});

while ($request->socketPerform()) {
    // do anything else when the request is processed
}

Guzzle PHP库还支持并发和异步请求。 - Simon East
1
Guzzle声称它有支持,但测试其postAsync方法看起来像是同步执行了150毫秒,然后异步执行了2毫秒。我花了一个多小时试图修复它,但没有成功 - 不建议使用。 - Velizar Hristov

6

不可以直接在PHP中实现。虽然PHP提供了很多调用URL的方式,但它没有提供对于每个请求/执行周期进行任何异步/线程处理的支持。发送URL请求(或SQL语句等)的任何方法都需要等待某种响应。因此,您需要在本地机器上运行某种次要系统来实现此功能(请谷歌搜索“php工作队列”)。


1
这里有一个 hack:https://dev59.com/questions/rHVD5IYBdhLWcg3wAGeD(Christian Davén的答案),但我同意使用队列是正确的方法。 - Chris Cinelli
1
我认为这个来自2009年的答案现在已经过时了。Guzzle PHP库现在支持并发和异步请求。 - Simon East

5
function make_request($url, $waitResult=true){
    $cmi = curl_multi_init();

    $curl = curl_init();
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);

    curl_multi_add_handle($cmi, $curl);

    $running = null;
    do {
        curl_multi_exec($cmi, $running);
        sleep(.1);
        if(!$waitResult)
        break;
    } while ($running > 0);
    curl_multi_remove_handle($cmi, $curl);
    if($waitResult){
        $curlInfos = curl_getinfo($curl);
        if((int) $curlInfos['http_code'] == 200){
            curl_multi_close($cmi);
            return curl_multi_getcontent($curl);
        }
    }
    curl_multi_close($cmi);
}

1
你可以让它返回一个对象,该对象允许你调用 getstatus()waitSend()waitResult()。这样,调用者可以通过循环调用来检查是否有结果,并且如果没有,继续进行其他任务。嗯,现在我想将 .net 中的 Task 移植到 php... - binki

4

如果您使用的是Linux环境,则可以使用PHP的exec命令来调用Linux Curl。以下是一个示例代码,它将进行异步HTTP post请求。

function _async_http_post($url, $json_string) {
  $run = "curl -X POST -H 'Content-Type: application/json'";
  $run.= " -d '" .$json_string. "' " . "'" . $url . "'";
  $run.= " > /dev/null 2>&1 &";
  exec($run, $output, $exit);
  return $exit == 0;
}

这段代码并不需要任何额外的PHP库,而且它可以在不到10毫秒的时间内完成http post请求。


1
这是一个非常糟糕的想法:exec 经常失败:想象一下,6/200个客户将无法收到他们支付预订的电子邮件确认... - HellBaby
这对我来说有效,只是我只需要一个ping来启动另一台服务器上的脚本。 我就像这样使用: _async_http_post($url,''); 在OVH共享服务器上也能正常工作...太棒了。 - Kilowog

3
没有人提到Guzzle,它是一个PHP HTTP客户端,可以轻松发送HTTP请求。它可以与或不使用Curl一起工作。它可以发送同步和异步请求。
$client = new GuzzleHttp\Client();
$promise = $client->requestAsync('GET', 'http://httpbin.org/get');
$promise->then(
    function (ResponseInterface $res) {
        echo $res->getStatusCode() . "\n";
    },
    function (RequestException $e) {
        echo $e->getMessage() . "\n";
        echo $e->getRequest()->getMethod();
    }
);

是的,这个帖子中的许多答案都相当老了,但是在2018年,Guzzle绝对是我遇到的最好的选择,感谢您的发布。 - Simon East

3
有趣的问题。我猜想你只是想在另一个服务器上触发一些进程或操作,但不关心结果,并希望你的脚本继续运行。cURL 中可能有一些东西可以实现这一点,但如果 cURL 无法实现,您可能需要考虑使用 exec() 在服务器上运行另一个脚本来执行调用(通常人们想要脚本调用的结果,因此我不确定 PHP 是否具有仅触发进程的能力)。通过 exec() 您可以运行一个 wget 或甚至另一个使用 file_get_conents() 发出请求的 PHP 脚本。

2

让我展示我的方法 :)

需要在服务器上安装nodejs

(我的服务器发送1000个https get请求只需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的解决方案。 - binki

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接