PHP cURL仅用于发送而不等待响应。

33

我需要一个PHP cURL配置,使得我的脚本能够发送请求并忽略API发送的响应。

curl_setopt($ch,CURLOPT_URL,$url);
curl_setopt($ch,CURLOPT_POST,count($fields));
curl_setopt($ch,CURLOPT_POSTFIELDS,$fields_string);
// curl_setopt($ch, CURLOPT_RETURNTRANSFER, false);
//curl_setopt($ch, CURLOPT_TIMEOUT_MS, 100);
$result = curl_exec($ch);
echo $result;
curl_close ($ch);

我尝试添加以下代码: // curl_setopt($ch, CURLOPT_RETURNTRANSFER, false); //curl_setopt($ch, CURLOPT_TIMEOUT_MS, 100);

但它并没有正常工作,API Web服务器没有接收到请求。

原因是我向API发送了大量请求,因此我的脚本非常缓慢,因为它会等待每个请求。

感谢任何帮助。


1
似乎比人们预期的要困难。这里有一篇关于这个问题和三种可能解决方案的好文章。https://segment.com/blog/how-to-make-async-requests-in-php/ TLDR:您可以使用 pfsockopen 打开持久套接字连接,或者分叉 curl 进程,或者如果您不关心实际请求的延迟(例如在日志场景中),您可以将请求记录到日志文件中,并使用后台进程(如 cron)发送带外请求使用日志条目。 - konrad
8个回答

30
发送者文件示例 ./ajax/sender.php 脚本发送POST请求 -> 它向主机发出完整的请求,但不等待服务器的响应:CURLOPT_HEADER(0)我们不需要来自服务器的头信息)和CURLOPT_RETURNTRANSFER(false)我们不需要来自服务器的数据。CURLOPT_TIMEOUT - 额外保护:我们只在发送后等待1毫秒以响应服务器,这是额外的保证,如果服务器保持我们,则不再等待任何毫秒。### 注意 ### HTTP1.1每个包最大16kb。HTTP2每个包36kb。如果POST更大,服务器将以多个连续的包发送= $SIZE%16kb
    $url = 'https://127.0.0.1/ajax/received.php';
    $curl = curl_init();                
    $post['test'] = 'examples daata'; // our data todo in received
    curl_setopt($curl, CURLOPT_URL, $url);
    curl_setopt ($curl, CURLOPT_POST, TRUE);
    curl_setopt ($curl, CURLOPT_POSTFIELDS, $post); 
    
    curl_setopt($curl, CURLOPT_USERAGENT, 'api');

    //curl_setopt($curl, CURLOPT_TIMEOUT, 1); //if your connect is longer than 1s it lose data in POST better is finish script in recevie
    curl_setopt($curl, CURLOPT_HEADER, 0);
    curl_setopt($curl,  CURLOPT_RETURNTRANSFER, false);
    curl_setopt($curl, CURLOPT_FORBID_REUSE, true);
    curl_setopt($curl, CURLOPT_CONNECTTIMEOUT, 1);
    curl_setopt($curl, CURLOPT_DNS_CACHE_TIMEOUT, 100); 
    
    curl_setopt($curl, CURLOPT_FRESH_CONNECT, true);
    
    curl_exec($curl);   
    
    curl_close($curl);  

收到文件示例 ./ajax/received.php
    ignore_user_abort(true); //if connect is close, we continue php script in background up to script will be end
    
            header("Connection: close\r\n"); 
            header("Content-Encoding: none\r\n"); 
            header("Content-Length: 1"); 
    ### we just close connect above if webbrowser, or request waiting on answer ( we know we set CURLOP to not wait) ###
ob_end_clean(); //just flush all content if exists to request. If server still waiting on answer.
            //HERE all script doing in background: Example
            $this->db->query('UPDATE new_hook_memory SET new=new+1 WHERE id=1');
 

如果你使用fastcgi,当fastcgi完成并且浏览器关闭连接时,脚本仍然会一直运行到结束。

如何结束脚本: 使用PHP mod_fcgi和fastcgi_finish_request();

对于Apache2:

ob_end_flush();
flush();

对于php-fpm

fastcgi_finish_request(); $this->db->query('UPDATE new_hook_memory SET new=new+1 WHERE id=1');

旧版本:


33
你介意在代码中加入任何注释吗?这是无用的。 - Tomáš Zato
1
这个可以工作,但是如果你设置了太小的超时时间(在我的情况下是10毫秒),它会在请求完全发送之前被中断。 - Klesun
一些解释:这段代码生成了一个cURL错误代码28:"在1000毫秒后超时,收到-1字节的0个字节"。 ($data是false)。该片段使用的技巧仅在忽略此错误,因为它不会传播到PHP。由cURL发送的查询将继续其生命周期。但是,您不知道查询是否已被主机接受。 - Skrol29
@skrol29 这个例子是针对包含IP或主机的单向流的,你需要信任你的主机或IP。这是一个简单的解决方案,可以避免等待响应。如果你想使用重要数据并确保安全,你应该使用不同的方法,比如使用RabbitMQ来进行更加可控的操作。即使你不需要知道curl的结果,它就像ping到PHP一样,用于执行类似计数+1的操作。 - Kamil Dąbrowski
现在使用PHP7,或者使用线程发送异步请求不是很容易吗? - NaturalBornCamper
fastcgi_finish_request -> 在使用前阅读此用例以获得更好的性能 https://www.php.net/manual/en/function.fastcgi-finish-request.php#119692 - Kamil Dąbrowski

10

这两种解决方法对我很有效

( 当然,虽然时间过去了很长时间,但我认为这个问题并没有过时 )

使用 file_get_contents:

  //url of php to be called

$url = "example.php/test?id=1";

  //this will set the minimum time to wait before proceed to the next line to 1 second

$ctx = stream_context_create(['http'=> ['timeout' => 1]]);

file_get_contents($url,null,$ctx);

  //the php will read this after 1 second 

使用cURL

  //url of php to be called

$url = "example.php/test?id=1";

$test = curl_init();

  //this will set the minimum time to wait before proceed to the next line to 100 milliseconds

curl_setopt_array($test,[CURLOPT_URL=>$url,CURLOPT_TIMEOUT_MS=>100,CURLOPT_RETURNTRANSFER=>TRUE]);

curl_exec($test);

  //this line will be executed after 100 milliseconds

curl_close ($test); 

在这两种情况下,被调用的PHP必须设置ignore_user_abort(true)

无论哪种情况,结果都不会被打印出来。但是请注意您设置的超时时间,它需要大于被调用的PHP开始产生结果所需的时间。


5
如果可能的话,你可以在后台运行wget(使用exec)。

这并没有解决问题。这相当于将curl放到后台运行,仍然会有一个进程等待响应,消耗计算资源。 - Bladt
不是最好的想法。 - Steve Moretz

1

在寻找一个真正可行的解决方案时,我遇到了一些挫折,最终我基于fsockopen()构建了一个服务,可以处理GET和POST请求,而且不会阻塞。

以下是服务类:

class NonBlockingHttpClientService {

    private $method = 'GET';
    private $params = [];
    private $port = 80;

    private $host;
    private $path;
    private $post_content;

    public function isPost(): bool
    {
        return ($this->method === 'POST');
    }

    public function setMethodToPost(): NonBlockingHttpClientService
    {
        $this->method = 'POST';

        return $this;
    }

    public function setPort(int $port): NonBlockingHttpClientService
    {
        $this->port = $port;

        return $this;
    }

    public function setParams(array $params): NonBlockingHttpClientService
    {
        $this->params = $params;

        return $this;
    }

    private function handleUrl(string $url): void
    {
        $url = str_replace(['https://', 'http://'], '', $url);

        $url_parts = explode('/', $url);

        if(count($url_parts) < 2) {
            $this->host = $url_parts[0];
            $this->path = '/';
        } else {
            $this->host = $url_parts[0];
            $this->path = str_replace($this->host, '', $url);
        }
    }

    private function handleParams(): void
    {
        if(empty($this->params)) return;

        if($this->isPost()) {
            $this->post_content = http_build_query($this->params);

        } else {
            /*
            if you want to specify the params as an array for GET request, they will just be
            appended to the path as a query string
            */
            if(strpos($this->path, '?') === false) {
                $this->path .= '?' . ltrim($this->arrayToQueryString($this->params), '&');
            } else {
                $this->path .= $this->arrayToQueryString($this->params);
            }
        }
    }

    private function arrayToQueryString(array $params): string
    {
        $string = '';

        foreach($params as $name => $value) {
            $string .= "&$name=" . urlencode($value);
        }

        return $string;
    }

    public function doRequest(string $url): bool
    {
        $this->handleUrl($url);
        $this->handleParams();

        $host = $this->host;
        $path = $this->path;

        $fp = fsockopen($host,  $this->port, $errno, $errstr, 1);

        if (!$fp) {
            $error_message = __CLASS__ . ": cannot open connection to $host$path : $errstr ($errno)";
            echo $error_message;
            error_log($error_message);

            return false;

        } else {
            fwrite($fp, $this->method . " $path HTTP/1.1\r\n");
            fwrite($fp, "Host: $host\r\n");

            if($this->isPost()) fwrite($fp, "Content-Type: application/x-www-form-urlencoded\r\n");
            if($this->isPost()) fwrite($fp, "Content-Length: " . strlen($this->post_content) . "\r\n");

            fwrite($fp, "Connection: close\r\n");
            fwrite($fp, "\r\n");

            if($this->isPost()) fwrite($fp, $this->post_content);

            return true;
        }
    }
}

这个可以像这样使用:

$req = new NonBlockingHttpClientService();
$req->setMethodToPost(); //default is GET, so just omit this for GET requests
$req->setParams([
    'test2' => 'aaaa', //if parameters are specified both with setParams() and in the query string, for GET requests, params specified with setParams() will take precedence
    'test3' => 'bbbb',
    'time' => date('H:i:s')
]);

$req->doRequest('test.localhost/some_path/slow_api.php?test1=value1&test2=value2');

而 slow_api.php 文件,可以长这样。
<?php
error_log('start');
sleep(10);
error_log(print_r($_REQUEST, 1) . 'end');

我发现通过监控(tail -f)错误日志来查看发生了什么更容易。


0
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 0);
curl_setopt($ch, CURLOPT_TIMEOUT_MS, 1);
curl_exec($ch);
curl_close($ch);

这对我很有效。
在 PHP 7.1.14 Windows 上测试过。


这个会打印响应,如何禁用它? - Muhammad Hassaan
@Hassaan:curl_setopt($ch, CURLOPT_RETURNTRANSFER, TRUE) - Marco Demaio
1
@MarcoDemaio 据我所知,这将使脚本等待响应。 - l00k
@l00k,我认为Hassaan在他的评论中问如何不打印curl_exec($ch);调用的结果。 - Marco Demaio

0
你如何判断请求是否成功?你需要等待服务器返回至少状态码才能确定。如果延迟是问题,可以使用curl多API并行执行多个请求。你应该能够设置一个写回调函数来中止接收返回数据一旦状态码已经返回。

2
非常老的答案,但是这个模式的重点在于你不关心它。想象一下低优先级的跟踪调用,您希望能够发送请求而不必关心响应,以便最小化服务器负载,处理大量微小的低重要性请求。 - Jarrod Christman

-2
现在有点晚了,但对于任何感兴趣的人来说,解决方案是将 CURLOPT_RETURNTRANSFER 设置为 TRUE,而不是 false。这样,curl_exec 函数会立即返回一个值,而不是等待请求完成后再返回 - 也就是说,它是异步而不是同步的。
示例:
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

2
Reza问了另外一件事。 - Stelian
4
如果你调用的服务器需要30秒才能给出响应和值,那么如何立即获得价值? - user5890979
3
Curl_setopt总是立即返回,因为选项需要在连接启动之前设置。这个回答没有意义,是错误的,也没有回答OP的问题。 - Arthur
1
正解是curl_exec(不是curl_setopt),它将根据使用curl_setopt设置的CURLOPT_RETURNTRANSFER的值立即返回或等待外部服务器响应。感谢指出这一点-我已经编辑了我的答案。 - lukenofurther
1
对于任何阅读此内容的人,这是完全错误的:CURLOPT_RETURNTRANSFER指示cURL在调用curl_exec时返回服务器的响应,而不仅仅在成功/失败时返回true|false它与等待请求完成与否无关 - Gonzalingui
显示剩余3条评论

-2
如果您使用Linux,您可以在PHP中使用shell_exec并将curl的结果发送到dev/null。通过这种方法,您不会收到任何答案,发送PHP后,它会进入下一行,Linux会在后台执行您的命令。如果答案对您不重要,您应该使用此方法。
以下是一个示例:
shell_exec("curl 'http://domian.com/message?text=$text' > /dev/null 2>/dev/null &")

注意:您可以根据需要添加任何curl选项,例如头部信息、POST方法或代理。


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