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匿名函数,将后续的执行逻辑传了进去。这么看,事情就变得简单多了。
<?php
/**
* class crawler
* path: /async/crawler.php
*/
class crawler
{
private $url;
private $tovisit = [];
public function __construct($url)
{
$this->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,只是牺牲可维护性和开发效率。
相关阅读:
如何用php7安装swoole扩展
swoole开发要点介绍
php异步多线程swoole用法实例
以上就是本篇文章的所有内容,同学们如果有疑问可以在下方评论区讨论哦~
以上就是用swoole异步抓取网页实战分享的详细内容。