php程序员都知道,使用php写的程序都是同步的,如何用php写一个异步程序呢,答案就是Swoole。这里以抓取网页内容为例,来展示如何用Swoole来编写异步程序。
php的同步程序
在写异步程序之前,不要着急,先用php实现一下同步的程序。
<?php /** * Class Crawler * Path: /Sync/Crawler.php */ class Crawler { private $url; private $toVisit = []; public function __construct($url) { $this->url = $url; } public function visitOneDegree() { $this->loadPageUrls(); $this->visitAll(); } private function loadPageUrls() { $content = $this->visit($this->url); $pattern = '#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i'; preg_match_all($pattern, $content, $matched); foreach ($matched[0] as $url) { if (in_array($url, $this->toVisit)) { continue; } $this->toVisit[] = $url; } } private function visitAll() { foreach ($this->toVisit as $url) { $this->visit($url); } } private function visit($url) { return @file_get_contents($url); } }
<?php /** * crawler.php */ require_once 'Sync/Crawler.php'; $start = microtime(true); $url = 'http://www.swoole.com/'; $ins = new Crawler($url); $ins->visitOneDegree(); $timeUsed = microtime(true) - $start; echo "time used: " . $timeUsed; /* output: time used: 6.2610177993774 */
Swoole实现异步爬虫初探
先参考一下官方的异步抓取页面怎么搞。
使用示例
Swoole\Async::dnsLookup("www.baidu.com", function ($domainName, $ip) { $cli = new swoole_http_client($ip, 80); $cli->setHeaders([ 'Host' => $domainName, "User-Agent" => 'Chrome/49.0.2587.3', 'Accept' => 'text/html,application/xhtml+xml,application/xml', 'Accept-Encoding' => 'gzip', ]); $cli->get('/index.html', function ($cli) { echo "Length: " . strlen($cli->body) . "\n"; echo $cli->body; }); });
貌似稍微改造一下同步的file_get_contents代码,就可以实现异步了,看起来成功轻而易举嘛。
于是,我们得到了下面的代码:
<?php /** * Class Crawler * Path: /Async/CrawlerV1.php */ class Crawler { private $url; private $toVisit = []; private $loaded = false; public function __construct($url) { $this->url = $url; } public function visitOneDegree() { $this->visit($this->url, true); $retryCount = 3; do { sleep(1); $retryCount--; } while ($retryCount > 0 && $this->loaded == false); $this->visitAll(); } private function loadPage($content) { $pattern = '#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i'; preg_match_all($pattern, $content, $matched); foreach ($matched[0] as $url) { if (in_array($url, $this->toVisit)) { continue; } $this->toVisit[] = $url; } } private function visitAll() { foreach ($this->toVisit as $url) { $this->visit($url); } } private function visit($url, $root = false) { $urlInfo = parse_url($url); Swoole\Async::dnsLookup($urlInfo['host'], function ($domainName, $ip) use($urlInfo, $root) { $cli = new swoole_http_client($ip, 80); $cli->setHeaders([ 'Host' => $domainName, "User-Agent" => 'Chrome/49.0.2587.3', 'Accept' => 'text/html,application/xhtml+xml,application/xml', 'Accept-Encoding' => 'gzip', ]); $cli->get($urlInfo['path'], function ($cli) use ($root) { if ($root) { $this->loadPage($cli->body); $this->loaded = true; } }); }); } }
<?php /** * crawler.php */ require_once 'Async/CrawlerV1.php'; $start = microtime(true); $url = 'http://www.swoole.com/'; $ins = new Crawler($url); $ins->visitOneDegree(); $timeUsed = microtime(true) - $start; echo "time used: " . $timeUsed; /* output: time used: 3.011773109436 */
结果运行了3秒。注意一下我的实现,在发起抓取首页的请求以后,我会隔一秒轮询一次结果,轮询三次还没有就结束了。这里的3秒好像是轮询了3次还没有结果导致的退出。
看来是我太急躁了,给人家的准备时间还不够充分。好吧,那我们把轮询次数改为10次,看看结果。
time used: 10.034232854843
此时我的心情,你懂的。
难道说是swoole的性能问题?为什么10秒还没有结果,难道是我的姿势不对?马克思老人家说过:“实践是检验真理的唯一标准”。看来需要debug一下才知道原因了。
于是,我在
$this->visitAll();
和
$this->loadPage($cli->body);
两处加了断点。最后发现总是先执行到visitAll(),再去执行loadPage()。
想了一下,大概明白原因了。到底是什么原因呢?
我期望的异步动态模型是这样的:
然而真实的场景不是这样的。通过调试,我大致了解到实际的模型应该是这样的:
也就是说,无论我怎么提高重试次数,数据永远不会准备好,数据只有在当前函数准备好以后,才会开始执行,这里的异步,只是减少了准备连接的时间。
那么问题来了,我该如何让程序在准备数据之后执行我期望的功能呢。
先看一下Swoole官方执行异步任务的代码是如何写的
$serv = new swoole_server("127.0.0.1", 9501); //设置异步任务的工作进程数量 $serv->set(array('task_worker_num' => 4)); $serv->on('receive', function($serv, $fd, $from_id, $data) { //投递异步任务 $task_id = $serv->task($data); echo "Dispath AsyncTask: id=$task_id\n"; }); //处理异步任务 $serv->on('task', function ($serv, $task_id, $from_id, $data) { echo "New AsyncTask[id=$task_id]".PHP_EOL; //返回任务执行的结果 $serv->finish("$data -> OK"); }); //处理异步任务的结果 $serv->on('finish', function ($serv, $task_id, $data) { echo "AsyncTask[$task_id] Finish: $data".PHP_EOL; }); $serv->start();
可以看到,官方是通过一个function匿名函数,将后续的执行逻辑传了进去。这么看,事情就变得简单多了。
url = $url; } public function visitOneDegree() { $this->visit($this->url, function ($content) { $this->loadPage($content); $this->visitAll(); }); } private function loadPage($content) { $pattern = '#((http|ftp)://(\S*?\.\S*?))([\s)\[\]{},;"\':<]|\.\s|$)#i'; preg_match_all($pattern, $content, $matched); foreach ($matched[0] as $url) { if (in_array($url, $this->toVisit)) { continue; } $this->toVisit[] = $url; } } private function visitAll() { foreach ($this->toVisit as $url) { $this->visit($url); } } private function visit($url, $callBack = null) { $urlInfo = parse_url($url); Swoole\Async::dnsLookup($urlInfo['host'], function ($domainName, $ip) use($urlInfo, $callBack) { if (!$ip) { return; } $cli = new swoole_http_client($ip, 80); $cli->setHeaders([ 'Host' => $domainName, "User-Agent" => 'Chrome/49.0.2587.3', 'Accept' => 'text/html,application/xhtml+xml,application/xml', 'Accept-Encoding' => 'gzip', ]); $cli->get($urlInfo['path'], function ($cli) use ($callBack) { if ($callBack) { call_user_func($callBack, $cli->body); } $cli->close(); }); }); } }
看了这段代码,竟然有种似曾相识的感觉,在nodejs开发中,随处可见的callback原来是有它的道理的。现在我才突然明白,原来callback的存在就是为了解决异步问题的。
执行了一下程序,竟然只用0.0007s,还没开始就已经结束了!异步的效率真的能提升这么多吗?答案当然是否定的,是我们的代码出问题了。
由于用了异步,没有等任务完全跑完,就已经执行了计算结束的时间的逻辑。看来又到了用callback的时候了。
/** Async/Crawler.php **/ public function visitOneDegree($callBack) { $this->visit($this->url, function ($content) use($callBack) { $this->loadPage($content); $this->visitAll(); call_user_func($callBack); }); }
<?php /** * crawler.php */ require_once 'Async/Crawler.php'; $start = microtime(true); $url = 'http://www.swoole.com/'; $ins = new Crawler($url); $ins->visitOneDegree(function () use($start) { $timeUsed = microtime(true) - $start; echo "time used: " . $timeUsed; }); /*output: time used: 0.068463802337646 */
现在来看,结果可信多了。
让我们比较一下同步的异步的差距,同步耗时6.26s,异步耗时0.068秒,差了整整6.192s。不,更准确地表述,应该是差了将近10倍!
当然,从效率上讲,异步远远高于同步的代码,但是从逻辑上讲,异步的逻辑比同步更绕,代码中会带来大量的callback,不便于理解。
Swoole官方里有一段关于异步与同步的选择的描述,非常中肯,分享给大家:
我们不赞成用异步回调的方式去做功能开发,传统的PHP同步方式实现功能和逻辑是最简单的,也是最佳的方案。像node.js这样到处callback,只是牺牲可维护性和开发效率。
相关阅读:
以上就是本篇文章的所有内容,同学们如果有疑问可以在下方评论区讨论哦~
以上是用Swoole异步抓取网页实战分享的详细内容。更多信息请关注PHP中文网其他相关文章!