Git Product home page Git Product logo

miscellany's Introduction

前言

PHP 中的代码执行都是从上到下的(同步),在需要请求第三方 API 时,通常使用的库都是同步且阻塞的,如果需要同时请求 API1 和 API2,那么脚本的耗时就是 API1 + API2,在查找了相关资料后发现 PHP 对于批量的 cURL 有底层函数支持 curl_multi_exec,使用这个函数之后最终的脚本耗时为 max(API1,API2).

但是由于 curl_multi_exec 的非阻塞调度都在 PHP 源码中,无法直观的感受非阻塞调度的流程,所以进一步研究了 socket_select 相关,同时结合 PHP8.1 中的 Fiber 写了几个 Demo,加深非阻塞、阻塞、异步、同步的理解。

模拟 API

首先创建一个耗时的 API 服务:

index.php

$execTime = $_GET['exec_time'] ?? 1;

sleep((int)$execTime);

echo "exec complete in {$execTime}s\n";

通过 GET 参数来决定这个服务耗时

服务需要部署到 nginx 中,php -S 生成的服务器不支持并发,会影响 Demo 的结果,nginx.conf 参考 ./async.conf

curl_multi_exec

curl-demo.php

use Minororange\AsyncSocket\Origin\MultiCurl;

require_once './vendor/autoload.php';


$startTime = microtime(true);
echo "start: {$startTime}\n";
$multiCurl = new MultiCurl([
    'http://localhost:8123?exec_time=1',
    'http://localhost:8123?exec_time=2',
    'http://localhost:8123?exec_time=3',
    'http://localhost:8123?exec_time=4',
    'http://localhost:8123?exec_time=5',
]);
$response = $multiCurl->request();
$spend = microtime(true) - $startTime;
echo "request complete in [$spend]s\n";

echo "responses:\n";

var_dump($response);

curl_multi_exec 函数相关其他操作比较多,所以封装了 MultiCurl,具体用法可查看该类源码

$ php curl-demo.php

# 以下是输出结果
start: 1663316982.7557
curl start at:[1663316982.7806]
curl complete in [6.0205311775208]s
curl start at:[1663316982.7807]
curl complete in [6.0203881263733]s
curl start at:[1663316982.7808]
curl complete in [6.0203499794006]s
curl start at:[1663316982.7809]
curl complete in [6.0202620029449]s
curl start at:[1663316982.781]
curl complete in [6.0201759338379]s
request complete in [6.0454359054565]s
responses:
array(5) {
  [0]=>
  string(20) "exec complete in 1s"
  [1]=>
  string(20) "exec complete in 2s"
  [2]=>
  string(20) "exec complete in 3s"
  [3]=>
  string(20) "exec complete in 4s"
  [4]=>
  string(20) "exec complete in 5s"
}

API 中最大耗时为 5s,5 个 API 如果串行,总耗时应该是 1+2+3+4+5 = 15s ,使用 curl_multi_exec 最终耗时 6s 左右

Socket

PHP 中的 socket 相关函数可参阅:https://www.php.net/manual/zh/ref.sockets.php

本项目主要使用以下几个函数:

  • socket_create: 创建一个socket
  • socket_set_nonblock:将 socket 设置为非阻塞
  • socket_connect:连接
  • socket_select:调用系统的 select 方法(结合非阻塞 IO 使用,NIO、BIO和 Selector的介绍可参阅:https://learnku.com/articles/65347)
  • socket_write:写入数据(发送数据)
  • socket_read:读取数据

no-fiber-socket-demo.php

$serverEntity = new ServerEntity();
$serverEntity->address = 'localhost';
$serverEntity->port = 8123;
/** @var Http[] $httpClients */
$httpClients = [];
$responses = [];
Timer::start("multi request");
for ($i = 1; $i <= 5; $i++) {
    $http = new Http(new Client($serverEntity, AF_INET, SOCK_STREAM, SOL_TCP));
    $httpClients[$i] = $http;
    Timer::start("request[{$i}]");
    $http->request(['exec_time' => $i]);
}

foreach ($httpClients as $i => $httpClient) {
    $responses[] = $httpClient->getResponse()->getBody();
    $httpClient->getClient()->close();
    Timer::end("request[{$i}]");
}

var_dump($responses);

Timer::end("multi request");
$ php no-fiber-socket-demo.php

# 以下是输出结果
multi request started,began time:[1663317900.7515]s
request[1] started,began time:[1663317900.7627]s
request[2] started,began time:[1663317900.7628]s
request[3] started,began time:[1663317900.7628]s
request[4] started,began time:[1663317900.7629]s
request[5] started,began time:[1663317900.763]s
request[1] completed,time usage:[1.018]s
request[2] completed,time usage:[2.7019]s
request[3] completed,time usage:[4.0148]s
request[4] completed,time usage:[4.015]s
request[5] completed,time usage:[5.0061]s
array(5) {
  [0]=>
  string(19) "exec complete in 1s"
  [1]=>
  string(19) "exec complete in 2s"
  [2]=>
  string(19) "exec complete in 3s"
  [3]=>
  string(19) "exec complete in 4s"
  [4]=>
  string(19) "exec complete in 5s"
}
multi request completed,time usage:[5.0177]s

运行结果与 curl_multi_exec 差不多

  • Client 请求时序图:

Client 请求时序图

  • 批量请求代码流程图:

Laravel

Socket 和 Fiber

Fiber 相关文档参阅:https://www.php.net/manual/zh/class.fiber.php

此项目主要使用的 Fiber 方法:

  • new Fiber: 新建一个纤程
  • Fiber::start: 启动纤程
  • Fiber::suspend:暂停纤程
  • Fiber::resume:恢复Fiber执行
  • Fiber::getCurrent:获取当前Fiber实例

fiber-socket-demo.php

$serverEntity = new ServerEntity();
$serverEntity->address = 'localhost';
$serverEntity->port = 8123;
$responses = [];
Timer::start("multi request");
for ($i = 1; $i <= 5; $i++) {
    $requestFiber = new \Fiber(function () use ($serverEntity, &$responses, $i) {
        $http = new Http($serverEntity, AF_INET, SOCK_STREAM, SOL_TCP);
        // 建立连接后,暂停当前,进入下一个循环
        FiberPool::getInstance()->enqueue("connect[{$i}]");
        Timer::start("request[{$i}]");
        $http->request(['exec_time' => $i]);
        // 发送数据后,暂停当前,进入下一个循环
        FiberPool::getInstance()->enqueue("send[{$i}]");
        $responses[] = $http->getResponse()->getBody();
        Timer::end("request[{$i}]");
    });
    $requestFiber->start();
}

FiberPool::getInstance()->run();

var_dump($responses);

Timer::end("multi request");

Demo 代码与 Socket 的代码大致相同,只是中间加入了 Fiber 的暂停功能,代码流程图如下:

Laravel

运行结果:

$ php fiber-socket-demo.php

# 以下是运行结果
multi request started,began time:[1663324710.2827]s
connect[1] await
connect[2] await
connect[3] await
connect[4] await
connect[5] await
connect[1] resume
request[1] started,began time:[1663324710.3021]s
send[1] await
connect[2] resume
request[2] started,began time:[1663324710.3023]s
send[2] await
connect[3] resume
request[3] started,began time:[1663324710.3023]s
send[3] await
connect[4] resume
request[4] started,began time:[1663324710.3023]s
send[4] await
connect[5] resume
request[5] started,began time:[1663324710.3023]s
send[5] await
send[1] resume
request[1] completed,time usage:[1.0376]s
send[2] resume
request[2] completed,time usage:[2.0132]s
send[3] resume
request[3] completed,time usage:[3.0138]s
send[4] resume
request[4] completed,time usage:[4.8346]s
send[5] resume
request[5] completed,time usage:[6.026]s
array(5) {
  [0]=>
  string(19) "exec complete in 1s"
  [1]=>
  string(19) "exec complete in 2s"
  [2]=>
  string(19) "exec complete in 3s"
  [3]=>
  string(19) "exec complete in 4s"
  [4]=>
  string(19) "exec complete in 5s"
}
multi request completed,time usage:[6.0459]s

miscellany's People

Contributors

minororange avatar

Stargazers

 avatar

Watchers

 avatar

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.