优秀的编程知识分享平台

网站首页 > 技术文章 正文

PHP中异步执行http请求(Guzzle And Curl)

nanyue 2024-08-07 18:56:24 技术文章 12 ℃

我认为没有使用线程的任何好的用例 在 PHP 中,但 还有一些工具(swoole、workerman以及衍生的工具)不容忽视!可以在 PHP 中异步执行(至少一件事)——开箱即用!

考虑这样一个例子:

你想向外部(一个或 4 个不同的)服务发出 4 个请求——所有 4 个请求的“时间成本”是多少?

有两种可能情况:

  1. 我们可以同步处理请求——那么总时间就是所有后续请求成本的总和
  2. 我们可以异步完成它们——总时间将等于最长请求的时间

请参见下图以可视化场景,假设我们必须发出 4 个请求,其中:

  • 第一个的响应是立即的(我们可以忽略延迟)
  • 第二个响应在 1 秒后出现
  • 第三次的回应——2秒
  • 第四次的回应——3秒

什么适用于 PHP?

让我们检查一下(您可以在下面找到所执行测试的解释,结果用了3s多):

docker compose run php
Sequence: 0 .. 3
...
end
Woke up after 0 second(s) of sleep!
Woke up after 1 second(s) of sleep!
Woke up after 2 second(s) of sleep!
Woke up after 3 second(s) of sleep!

real    0m3.099s

这里究竟测试了什么?

作为“外部服务”,我使用了我称之为“睡眠服务器”的工具——你可以向该工具(服务)发送一个请求,并将秒数作为参数,它会在请求的时间后做出响应(更多信息和示例 可以在项目的自述文件中找到)。

测试PHP代码:

$client = new GuzzleHttp\Client();

for($i = $sequenceStart; $i <= $sequenceStop; $i++) {
    Utils::task(
        fn() => $client->getAsync("{$sleepingUrl}/{$i}")
            ->then($printResponse)
            ->wait()
    );
}
echo 'end' . PHP_EOL;

所有这些都被 docker-compose 绑定了:

services:
  sleeping:
    build: "https://github.com/lbacik/sleeping-server.git#main"

  php:
    build: ./php
    environment:
      SLEEPING_URL: "http://sleeping:3000"
    depends_on:
      - sleeping
    ...

代码示例:https://github.com/lbacik/php-async

让我们将 PHP(php:8.1 )结果与 JS 代码进行比较。

代码的JS版本:

for (let i=sequenceStart; i<=sequenceStop; i++) {
    fetch(`${sleepingUrl}/${i}`)
        .then(response => response.text())
        .then(result => console.log(result))
}
console.log('end')

测试

 docker compose run js
Sequence: 0 .. 3      
...
end
Woke up after 0 second(s) of sleep!
Woke up after 1 second(s) of sleep!
Woke up after 2 second(s) of sleep!
Woke up after 3 second(s) of sleep!

real    0m3.182s

结果几乎相同!


事情要解释

我认为有两个(关于 PHP 代码):

为什么要执行这些请求? (请注意脚本输出中打印的“end”字符串——它是脚本的最后一行!)

为什么这些请求是异步执行的?

第一个有点奇怪——我的意思是——答案(至少对我而言)。 让我解释:

我们从 Util::task() 开始,因为它已在我的示例中使用 - 检查该方法(它在 GuzzleHttp\Promise 命名空间中声明)! 你可以在那里找到类似 $queue = self::queue()

按照此跟踪并检查 queue() 方法。 如果队列不存在,则会创建它: $queue = new TaskQueue() (在我们的例子中会发生什么)——让我们打开 TaskQueue 构造函数:

public function __construct($withShutdown = true)
{
    if ($withShutdown) {
        register_shutdown_function(function () {
            if ($this->enableShutdown) {
                // Only run the tasks if an E_ERROR didn't occur.
                $err = error_get_last();
                if (!$err || ($err['type'] ^ E_ERROR)) {
                    $this->run();
                }
            }
        });
    }
}

register_shutdown_function!起初它让我感到惊讶,如果 PHP 中有这样的可能性,为什么不使用它们(但是,我对此不确定)。

现在:

为什么这些请求是异步执行的?

因为 curl 库允许这样做!

-Z, —平行

与常规串行方式相比,使 curl 并行执行其传输。

PHP 也使用这个库——php 解释器可以用它编译,也可以作为扩展添加——PHP 扩展可以通过 php -m 检查:

php -m
...
curl
...

Curl PHP 扩展提供了对系统 curl 库的访问——你可以使用 cli curl 接口(如果我检查正确的话):

docker run --rm -ti php:8.1 bash
...
# ldd /usr/local/bin/php | grep curl
 libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 ...
# ldd /usr/bin/curl | grep libcurl
 libcurl.so.4 => /usr/lib/x86_64-linux-gnu/libcurl.so.4 ...

它是一个用 C编写的工具,可以利用线程并且 PHP API 提供了一种利用 libcurl 库提供的多线程功能的方法!

接下来是下一部分——如果您想直接使用此功能,我的意思是按照 PHP API 描述它的方式,那么讨论的代码可能如下所示:

<?php

require_once __DIR__ . '/../vendor/autoload.php';

$sleepingUrl = getenv('SLEEPING_URL') ?: "http://localhost:3000";

$multi = curl_multi_init();

$stillRunning = null;

for ($i = 0; $i <= 3; $i++) {
    $curl = curl_init("{$sleepingUrl}/{$i}");
    curl_multi_add_handle($multi, $curl);
}

do {
    $status = curl_multi_exec($multi, $stillRunning);
    if ($stillRunning) {
        curl_multi_select($multi);
    }
} while ($stillRunning && $status == CURLM_OK);

上面的脚本可以在这里找到并由(对不起那个小cli-monster)执行/测试:

docker compose run \
  --entrypoint "/bin/bash -c \"time php bin/fetch_mcurl.php\"" php

你可以像那样使用它——但它看起来不会比上面提供的解决方案复杂得多:

Utils::task(
    fn() => $client->getAsync("{$sleepingUrl}/{$i}")
        ->then($printResponse)
        ->wait()
);

对于我来说,通过 Guzzle 开发人员实现的 promise 接口的使用看起来好多了

最近发表
标签列表