PHP如何发送HTTP请求并且不等待响应?

52

我希望从PHP发送一个HTTP GET请求。例如:

http://tracker.example.com?product_number=5230&price=123.52

想法是进行服务器端的网站分析:服务器直接向另一个服务器发送跟踪信息,而不是从JavaScript发送跟踪信息到服务器。

要求:

  • 请求应尽可能快地完成,以避免明显延迟PHP页面的处理。

  • tracker.example.com的响应不需要被检查。以下是一些来自tracker.example.com的可能的响应示例:

    • 200:没问题,但没必要检查。

    • 404:很遗憾,但 - 再次强调 - 无需检查。

    • 301:虽然重定向是合适的,但它会延迟PHP页面的处理,因此不要这样做。

    简言之:所有响应都可以被丢弃。

解决方案的想法:

  • 在现已删除的答案中,有人建议从PHP中调用命令行curl来执行shell进程。这似乎是个好主意,只是我不知道在高负载下fork许多shell进程是否明智。

  • 我找到了php-ga,一个用于从PHP进行服务器端Google Analytics的软件包。在该项目的页面上,提到:"可以配置为[...]使用非阻塞请求。"到目前为止,我还没有时间调查php-ga内部使用的方法,但这个方法可能是最佳选择!

简而言之:从PHP进行通用的服务器端跟踪/分析的最佳解决方案是什么。


1
madflow:因为在追踪过程中不应该减慢页面加载时间。如果跟踪器失败,那就倒霉了 - 但没必要每次都检查。 - feklee
1
@feklee 抱歉让您等待了这么久,我已经回答了(但内容并不是很满意)。如果您想要,我可以编辑一下,并提供更多信息(比如一些代码示例)。我从周五晚上开始准备回答,但最终提供了太多的信息(而且结构不够清晰)。 - Khez
1
可能有用的内容:https://segment.io/blog/how-to-make-async-requests-in-php/ 和 PHP 异步调用?/(也许第二个链接是重复的?之后再仔细看。) - Jeremy
@JeremyBanks 这篇segment.io的文章看起来非常有趣 - 谢谢!请注意,我不需要回复。 - feklee
它使用stream_set_blocking,但影响的是对流的读写,而不是“http-ping-ing”。 - Khez
显示剩余9条评论
10个回答

37

很不幸,PHP被定义为阻塞。虽然对于你通常处理的大多数功能和操作来说这是正确的,但当前情况是不同的。

我喜欢称之为HTTP-Ping的过程,要求你只访问特定的URI,强制特定的服务器启动其内部逻辑。一些功能允许你实现类似于此HTTP-ping的操作,而无需等待响应。

请注意,ping一个URL的过程分为两个步骤:

  1. 解析DNS
  2. 发出请求

一旦解析DNS并建立连接,发出请求应该会相当快,但并没有太多方法可以加快DNS的解析。

一些进行http-ping的方法包括:

  1. cURL,通过将CONNECTION_TIMEOUT设置为较低的值
  2. fsockopen,在写入后立即关闭
  3. stream_socket_client(与fsockopen相同),还添加了STREAM_CLIENT_ASYNC_CONNECT

虽然在DNS解析期间,cURLfsockopen都是阻塞的。但我注意到fsockopen明显更快,即使在最坏的情况下也是如此。

另一方面,stream_socket_client应该可以解决DNS解析问题,并且在这种情况下应该是最优的解决方案,但我没能让它起作用。

最后一个解决方案是启动另一个线程/进程来完成此操作。可以尝试通过系统调用来实现,但也可以通过fork当前进程来实现。不幸的是,在无法控制 PHP 运行环境的应用程序中,这两种方法都不太安全。

系统调用往往被阻塞,而 pcntl 默认未启用。


1
实际上,我可以通过指定 IP 地址来避免 DNS 查询。只是那时候还没有想到。 - feklee
@feklee 想在帖子中提到它,但是在接近结尾时我已经在想你在服务器上有什么访问权限。不过要小心 DNS 更改! - Khez
我并不完全理解访问权限与指定IP有什么关系(除了我也可以在/etc/hosts中指定它),但我确实拥有完全的访问权限(root)。无论如何,一个通用的答案显然最适合Stack Overflow。 - feklee
Khez,我不得不说这是一个很美妙的解决方案。甚至可能没有插件也有实现PHP基本异步的潜力。 - user3186555
@DaMaxContent 嗯,异步已经是黑客规范的一部分了,如果PHP 7.x也采用它,我不会感到惊讶...但我很高兴你发现这个解决方案“美丽”:D - Khez
嗨@Khez,不介意分享一下现在哪种解决方案很好,而且不使用任何第三方插件吗? - Haizad Annuar

22

我会这样称呼tracker.example.com:

get_headers('http://tracker.example.com?product_number=5230&price=123.52');

并且在跟踪器脚本中:

ob_end_clean();
ignore_user_abort(true);
ob_start();
header("Connection: close");
header("Content-Length: " . ob_get_length());
ob_end_flush();
flush();

// from here the response has been sent. you can now wait as long as you want and do some tracking stuff 

sleep(5); //wait 5 seconds
do_some_stuff();
exit;

2
“ignore_user_abort()”需要一个值(真),否则它只会返回其当前值。 - Berend
它在我的本地WAMP服务器上运行良好,但在我的服务器上却不行。我认为我的服务器需要配置。 - Amir Surnay

16

我为快速发送GET请求并无需等待响应的功能进行了实现:

function fast_request($url)
{
    $parts=parse_url($url);
    $fp = fsockopen($parts['host'],isset($parts['port'])?$parts['port']:80,$errno, $errstr, 30);
    $out = "GET ".$parts['path']." HTTP/1.1\r\n";
    $out.= "Host: ".$parts['host']."\r\n";
    $out.= "Content-Length: 0"."\r\n";
    $out.= "Connection: Close\r\n\r\n";

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

3
如果您想在请求中添加查询参数,请将“GET”行更改为以下内容:$out = "GET ".$parts['path'] . "?" . $parts['query'] . " HTTP/1.1\r\n"; - xyz
2
简单而优雅的解决方案。谢谢。 - Miguel Rão Vieira

6
我们曾经使用 fsockopenfwrite 结合的方式,但某天它突然不工作了,或者有时它表现得比较间歇性。经过一番研究和测试,如果你启用了 fopen 装饰器,我最终使用了 file_get_contentsstream_context_create 函数,并将超时设置为每秒钟的1/100。超时参数可以接收浮点值(https://www.php.net/manual/en/context.http.php)。我将其包装在 try...catch 块中,以便它失败后可以静默处理。它非常适合我们的目的。如果需要,您可以在 catch 中记录日志信息。超时是关键,如果您不想函数阻塞运行时。
function fetchWithoutResponseURL( $url )
{

    $context = stream_context_create([
        "http" => [
            "method"=>"GET",
            "timeout" => .01
            ]
        ]
    );

    try {
        file_get_contents($url, 0, $context);
    }catch( Exception $e ){
        // Fail silently
    }
}

如果SSL握手和其他连接准备步骤在10毫秒内未完成,则无法启动连接。实际上,所有依赖于连接超时的解决方案都会失败,如果您针对目标脚本中的某段代码,并且它未能在此有限时间内运行,则也会失败。为确保目标脚本完全运行,您必须等待连接关闭。 - Semra

4

对于那些使用WordPress作为后端的人来说,这很简单:

wp_remote_get($url, array(blocking=>false));


1
谢谢,这是一个非常好的提示,正是我在寻找的!通过CURL提交电子邮件以通过Autopilot发送需要花费2到12秒的时间,但使用这种方法可以将时间稳定降至约2秒。当然,这意味着我们无法看到POST请求的结果-但是我从未对Autopilot的响应做任何事情,所以在我的情况下这并不重要。 - JoLoCo

3
你可以使用 shell_exec 和命令行的 curl。
例如,参见这个问题

这看起来是一个不错的解决方案。只是我想知道,当许多用户访问页面时,产生大量的 shell 进程是否会有问题。 - feklee
你能详细阐述一下这个答案吗?(请参见对问题所做的编辑) - Shog9

3

在研究类似问题时来到这里。如果您有一个数据库连接,另一种可能性是将请求详情快速放入表格中,然后有一个单独基于cron的进程定期扫描该表格以处理新记录,并进行跟踪请求,从而使您的Web应用程序无需自己进行HTTP请求。


实际上,在这个应用程序中有类似的东西。但它不是基于Cron的(我尽量避免使用Cron)。它是一个守护进程,从数据库中读取的URL下载,在一个无限循环中。然而,对于给定的用例,利用该守护进程和相关的数据库感觉过于复杂。 - feklee

-1

我需要做类似的事情,只需ping一个url并丢弃所有响应。我使用了proc_open命令,它允许您使用proc_close立即结束进程。我假设您的服务器上已安装lynx:

<?php    
function ping($url) {
      $proc = proc_open("lynx $url",[],$pipes);
      proc_close($proc);
    }
?>

它会等待命令完全执行。 - Semra

-1

你可以直接使用CURL来实现这个功能。

我曾经使用非常短的超时时间(CURLOPT_TIMEOUT_MS)或者使用curl_multi_exec来实现它。

请注意:最终我放弃了这种方法,因为并不是每个请求都能正确地完成。这可能是由于我的服务器问题,尽管我无法排除curl失败的可能性。


2
难道请求失败的原因不是因为在发送请求之前必须建立连接吗?就目前而言,将CURLOPT_TIMEOUT_MS设置为极低的值是不明智的。 - feklee

-2
<?php
// Create a stream
$opts = array(
  'http'=>array(
    'method'=>"GET",
    'header'=>"Accept-language: en" 
  )
);

 $context = stream_context_create($opts);

// Open the file using the HTTP headers set above
$file = file_get_contents('http://tracker.example.com?product_number=5230&price=123.52', false, $context);
?>

5
或许可以解释一下你的代码是如何工作的,并说明它为什么能够满足提问者的需求? - cryptic_star
我非常确定这将完成整个请求并等待响应后再继续,不是吗? - Yann Chabot

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