每天学习一点点,成功增加一大步

PHP 巧用curl 并发减少后端访问时间

PHP zhanghui 564℃

首先,先了解下 PHP中的curl多线程函数:

http://php.net/manual/zh/ref.curl.php

  • curl_multi_add_handle — 向curl批处理会话中添加单独的curl句柄
  • curl_multi_close — 关闭一组cURL句柄
  • curl_multi_exec — 运行当前 cURL 句柄的子连接
  • curl_multi_getcontent — 如果设置了CURLOPT_RETURNTRANSFER,则返回获取的输出的文本流
  • curl_multi_info_read — 获取当前解析的cURL的相关传输信息
  • curl_multi_init — 返回一个新cURL批处理句柄
  • curl_multi_remove_handle — 移除curl批处理句柄资源中的某个句柄资源
  • curl_multi_select — 等待所有cURL批处理中的活动连接
  • curl_multi_setopt — 为 cURL 并行处理设置一个选项
  • curl_multi_strerror — Return string describing error code

步骤总结如下:

  • 第一步:调用curl_multi_init
  • 第二步:循环调用curl_multi_add_handle
    这一步需要注意的是,curl_multi_add_handle的第二个参数是由curl_init而来的子handle。
  • 第三步:持续调用curl_multi_exec
  • 第四步:根据需要循环调用curl_multi_getcontent获取结果
  • 第五步:调用curl_multi_remove_handle,并为每个字handle调用curl_close
  • 第六步:调用curl_multi_close

有一个网上找的简单例子,其作者称为dirty的例子,(稍后我会说明为何dirty):

 /*
Here's a quick and dirty example for curl-multi from PHP, tested on PHP 5.0.0RC1 CLI / FreeBSD 5.2.1
*/
$connomains = array(
"http://www.cnn.com/",
"http://www.canada.com/",
"http://www.yahoo.com/"
);
$mh = curl_multi_init();
foreach ($connomains as $i => $url) {
 $conn[$i]=curl_init($url);
 curl_setopt($conn[$i],CURLOPT_RETURNTRANSFER,1);
 curl_multi_add_handle ($mh,$conn[$i]);
}
do {
 $n=curl_multi_exec($mh,$active);
} while ($active);
foreach ($connomains as $i => $url) {
 $res[$i]=curl_multi_getcontent($conn[$i]);
 curl_close($conn[$i]);
}
print_r($res);

整个使用过程差不多就是这样,但是,这个简单代码有个致命弱点,就是在do循环的那段,在整个url请求期间是个死循环,它会轻易导致CPU占用100%。
现在我们来改进它,这里要用到的函数是 curl_multi_select,关于这个函数的使用请看官方的手册 ,虽然C的curl库对select有说明,但是,php里的接口和用法确与C中有不同。
把上面do的那段改成下面这样:

do {
 $mrc = curl_multi_exec($mh,$active);
} while ($mrc == CURLM_CALL_MULTI_PERFORM);
while ($active and $mrc == CURLM_OK) {
 if (curl_multi_select($mh) != -1) {
  do {
   $mrc = curl_multi_exec($mh, $active);
  } while ($mrc == CURLM_CALL_MULTI_PERFORM);
 }
}

因为$active要等全部url数据接受完毕才变成false,所以这里用到了 curl_multi_exec 的返回值判断是否还有数据,当有数据的时候就不停调用curl_multi_exec,暂时没有数据就进入select阶段,新数据一来就可以被唤醒继续执行。这里的好处就是CPU的无谓消耗没有了。

另外:还有一些细节的地方可能有时候要遇到,比如控制每一个请求的超时时间,在 curl_multi_add_handle 之前通过 curl_setopt 去做:

curl_setopt($ch, CURLOPT_TIMEOUT, $timeout);

判断是否超时了或者其他错误,在 curl_multi_getcontent 之前用:

 curl_error($conn[$i]);

通常情况下 PHP 中的 cURL 是阻塞运行的,就是说创建一个 cURL 请求以后必须等它执行成功或者超时才会执行下一个请求,curl_multi_* 系列函数使并发访问成功可能,PHP 文档对这个函数的介绍不太详细,用法如下:

 $requests = array('http://www.baidu.com', 'http://www.google.com');
$main = curl_multi_init();
$handles = array();
$results = array();
$errors = array();
$info = array();
$count = count($requests);
for ($i = 0; $i < $count; $i++) {
    $handles[$i] = curl_init($requests[$i]);
    var_dump($requests[$i]);
    curl_setopt($handles[$i], CURLOPT_URL, $requests[$i]);
    curl_setopt($handles[$i], CURLOPT_RETURNTRANSFER, 1);
    curl_multi_add_handle($main, $handles[$i]);
}
$running = 0;
do {
    curl_multi_exec($main, $running);
} while ($running > 0);
for ($i = 0; $i < $count; $i++) {
    $results[] = curl_multi_getcontent($handles[$i]);
    $errors[] = curl_error($handles[$i]);
    $info[] = curl_getinfo($handles[$i]);
    curl_multi_remove_handle($main, $handles[$i]);
}
curl_multi_close($main);
var_dump($results);
var_dump($errors);
var_dump($info);

经过封装的实例代码:

<?php
/*****************************************************
CURL 非阻塞调用类
Auther: Linvo
Copyright(C) 2010/10/21
*******************************************************/
// 使用范例
// 传入参数说明
// url  请求地址
// data POST方式数据
//并发调用
$param1 = array(
array(
'url' => "http://www.baidu.com",
),
array(
'url' => "http://www.so.com",
),
array(
'url' => "http://www.sohu.com",
),
array(
'url' => "http://www.sina.com",
),
array(
'url' => "https://www.sogou.com",
),
array(
'url' => "http://www.youdao.com/",
),
array(
'url' => "http://www.baidu.com",
),
array(
'url' => "http://www.so.com",
),
array(
'url' => "http://www.sohu.com",
),
array(
'url' => "http://www.sina.com",
),
array(
'url' => "https://www.sogou.com",
),
array(
'url' => "http://www.youdao.com/",
),
);
//单个调用
$param2 = array(
'url' => "http://localhost/a.php?s=0",
'data' => array('aaa' => 1, 'bbb' => 2),
);
//单个调用(GET简便方式)
$param3 = 'http://localhost/a.php?s=2';
$ac = new AsyncCURL();
$ac->set_param($param1);
$ret = $ac->send();
//返回值为请求参数顺序的结果数组(元素值为False表示请求错误)
var_dump($ret);
class AsyncCURL
{
/**
* 是否需要返回HTTP头信息
*/
public $curlopt_header = 0;
/**
* 单个CURL调用超时限制
*/
public $curlopt_timeout = 20;
private $param = array();
/**
* 构造函数(可直接传入请求参数)
*
* @param array 可选
* @return void
*/
public function __construct($param = False)
{
if ($param !== False)
{
$this->param = $this->init_param($param);
}
}
/**
* 设置请求参数
*
* @param array
* @return void
*/
public function set_param($param)
{
$this->param = $this->init_param($param);
}
/**
* 发送请求
*
* @return array
*/
public function send()
{
if(!is_array($this->param) || !count($this->param))
{
return False;
}
$curl = $ret = array();
$handle = curl_multi_init();
foreach ($this->param as $k => $v)
{
$param = $this->check_param($v);
if (!$param) $curl[$k] = False;
else $curl[$k] = $this->add_handle($handle, $param);
}
$this->exec_handle($handle);
foreach ($this->param as $k => $v)
{
if ($curl[$k])
{
$ret[$k] = curl_multi_getcontent($curl[$k]);
curl_multi_remove_handle($handle, $curl[$k]);
} else {
$ret[$k] = False;
}
}
curl_multi_close($handle);
return $ret;
}
//以下为私有方法
private function init_param($param)
{
$ret = False;
if (isset($param['url']))
{
$ret = array($param);
} else {
$ret = isset($param[0]) ? $param : False;
}
return $ret;
}
private function check_param($param = array())
{
$ret = array();
if (is_string($param))
{
$url = $param;
} else {
extract($param);
}
if (isset($url))
{
$url = trim($url);
$url = stripos($url, 'http://') === 0 ? $url : NULL;
}
if (isset($data) && is_array($data) && !empty($data))
{
$method = 'POST';
} else {
$method = 'GET';
unset($data);
}
if (isset($url)) $ret['url'] = $url;
if (isset($method)) $ret['method'] = $method;
if (isset($data)) $ret['data'] = $data;
$ret = isset($url) ? $ret : False;
return $ret;
}
private function add_handle($handle, $param)
{
$curl = curl_init();
curl_setopt($curl, CURLOPT_URL, $param['url']);
curl_setopt($curl, CURLOPT_HEADER, $this->curlopt_header);
curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($curl, CURLOPT_TIMEOUT, $this->curlopt_timeout);
if ($param['method'] == 'POST')
{
curl_setopt($curl, CURLOPT_POST, 1);
curl_setopt($curl, CURLOPT_POSTFIELDS, $param['data']);
}
curl_multi_add_handle($handle, $curl);
return $curl;
}
private function exec_handle($handle)
{
$flag = null;
do {
curl_multi_exec($handle, $flag);
} while ($flag > 0);
}
}

转载请注明:隨習筆記 » PHP 巧用curl 并发减少后端访问时间