发了几个文章,但都没人评论,所以哥也不知道到底大家觉得是好是坏,欢迎给差评,这样才能提高啊亲!踊跃回复吧!
ND公司的内网网络非常的差,对于我们PHPer工作人员来说有时真是噩梦,但这场噩梦没那么容易醒呵呵。
前段时间要采集应用包,算出MD5,以后方便做病毒比对,但是每个包都只能下载到 6MB 左右就断了,我去年买了个表啊!还好网络搜索是强大的!经过一翻折腾还是让我搞定了这件事,于是有了这篇教程,跟大家分享下,代码在TP框架上写的,所以部分代码如果你们不懂,请翻翻TP手册,很简单的。让我们开始吧:
(转载请注明出处:福州 郑洪耕 QQ_823339113)
网上关于php实现断点续传,一般是用 header 函数设置 range 来实现,代码大概是下面这样的:
Header("Content-Length: " . ($fsize - $start));
Header("Content-Ranges: bytes" . $start . "-" . ($fsize - 1) . "/" . $fsize);
但有时不得不用curl来下载。看了curl手册发现个参数:CURLOPT_RESUME_FROM 可以设置从哪个偏移地址开始下载,所以就有了实现思路如下:
先检测下载资源是否存在,存在时 --> 开始下载,然后查看返回结果 --> 返回结果为失败时,取得这次已经下载文件片段的大小,然后做为偏移量继续用curl下载 --> 保存每个下载的文件的文件片段名字,在全部下载完成后合并文件。
实现过程还用到了哥自己实现的进程锁机制,大家也讨论下有没有更好的方案。
过程就是这样了,先准备好几个函数,用来检测资源是否存在(http状态)、用CURL下载资源、合并下载的文件片段:
function downUrl($url, $saveFile, $offset = 0, $proxy = '') {
$h_curl = curl_init();
$h_file = fopen($saveFile, 'wb');
curl_setopt($h_curl, CURLOPT_HEADER, 0);
curl_setopt($h_curl, CURLOPT_FOLLOWLOCATION, 1);
curl_setopt($h_curl, CURLOPT_TIMEOUT, 60);
curl_setopt($h_curl, CURLOPT_URL, $url);
curl_setopt($h_curl, CURLOPT_FILE, $h_file);
curl_setopt($h_curl, CURLOPT_PROXY, $proxy);
curl_setopt($h_curl, CURLOPT_SSL_VERIFYPEER, false); // 阻止对证书的合法性的检查
curl_setopt($h_curl, CURLOPT_SSL_VERIFYHOST, false); // 从证书中检查SSL加密算法是否存在
curl_setopt($h_curl, CURLOPT_RESUME_FROM, $offset);
//curl_setopt($h_curl, CURLOPT_RETURNTRANSFER, true);
$curl_success = curl_exec($h_curl);
fclose($h_file);
curl_close($h_curl);
return $curl_success;
}
//合并两个文件,保存到$mainFile
function mergeFile($mainFile, $partFile) {
$h_mainFile = fopen($mainFile, 'ab+');
$h_partFile = fopen($partFile, 'rb');
$content = fread($h_partFile, filesize($partFile));
fclose($h_partFile);
fwrite($h_mainFile, $content);
fclose($h_mainFile);
}
function getHttpStatus($p_url) {
$h_curl = curl_init();
$url = trim($p_url);
curl_setopt($h_curl, CURLOPT_URL, $url);
curl_setopt($h_curl, CURLOPT_HEADER, 1); //获取Header
curl_setopt($h_curl, CURLOPT_NOBODY,true); //Body不要
curl_setopt($h_curl, CURLOPT_RETURNTRANSFER, 1); //数据存到字符串,别直接输出到屏幕
$data = curl_exec($h_curl);
$statCode = curl_getinfo($h_curl, CURLINFO_HTTP_CODE); //HTTPSTAT码
curl_close($h_curl);
return $statCode;
}
然后就是核心代码部分了(这是TP里的一个控制器,涉及的TP知识其实不多,稍微查下手册就知道了):
<?php
/**
*
* 更新App的MD5值
*
* @author UID 2013.04.26
*
*/
class AppHashAction extends Action{
//App 表里没有MD5值、是外链的,下载软件包计算MD5值
private $md5FileToWrite;
public function _initialize() {
$this->md5FileToWrite = BASE_PATH . '/urlmd5.txt';
@rename($this->md5FileToWrite,
$this->md5FileToWrite . $_SERVER['REQUEST_TIME']);
@rename($this->md5FileToWrite . '.php',
$this->md5FileToWrite . '.php' . $_SERVER['REQUEST_TIME']);
}
public function index() {
$lockFile = BASE_PATH . '/stop.lock';
$mMd5 = M('AppMd5');
$where = array(
'app_md5' => array('eq', ''),
);
$field = 'app_id,app_url';
$order = 'app_size asc';
$appList = $mMd5->where($where)->field($field)->order($order)->select();
$this->_finish_request(0);
foreach($appList as $idx => $app) {
if(file_exists($lockFile)) {//进程锁机制,可以防多进程跑同一任务,也可以在中途删除锁文件,让进程停止(当然主要目的是要它停止,那放在粒度最小的位置去)
@unlink($lockFile);
break;
}
$appUrl = trim($app['app_url']);
if(200 != getHttpStatus($appUrl)) {//这里只检测200状态
continue;
}
$appName = $app['app_id'] . '_' . md5($appUrl);
$saveFile = $this->_genDownPath() . $appName;
$downResult = false;
$downSize = 0;
//用来保存下载资源的各片段文件名
$saveFileSplit = array();
$saveCount = 0;
do {
$fileSplitName = empty($saveCount) ?
$saveFile :
$saveFile . '_' . $saveCount . '_' . $downSize;
$saveFileSplit[$saveCount++] = $fileSplitName;
$downResult = downUrl($appUrl, $fileSplitName, $downSize);
if(file_exists($fileSplitName)) {
$downSize += filesize($fileSplitName);
}
} while(!$downResult);
$this->_mergeDown($saveFileSplit, true);
$md5 = md5_file($saveFile);
@unlink($saveFile);
$mMd5 ->where(array('app_id' => intval($app['app_id']),))
->save(array('app_md5' => $md5,));
file_put_contents('updateAppMd5.txt', $mMd5->_sql() . PHP_EOL . ';', FILE_APPEND);
$this->_writeMd5File($idx,
$app['app_id'],
$md5,
$appUrl);
}
}
/**
* 网络原因造成下载不完整,分段下载,此方法进行合并
*
* @param array fileParts 第一个为实际要保存的文件名,后续的每个按顺序合并到第一个
*/
private function _mergeDown($fileParts, $rmTrash = false) {
if(count($fileParts) > 1) {
$saveFile = array_shift($fileParts);
foreach($fileParts as $filePart) {
mergeFile($saveFile, $filePart);
$rmTrash && @unlink($filePart);
}
}
}
private function _writeMd5File($idx, $appid, $md5, $appurl) {
$appMd5 = array(
'app_id' => intval($appid),
'app_md5' => $md5,
'app_url' => $appurl,
);
file_put_contents($this->md5FileToWrite,
"{$appid} {$md5} {$appurl}" . "\r\n",
FILE_APPEND);
file_put_contents($this->md5FileToWrite . '.php',
$idx . '=>' . var_export($appMd5, true) . ",\r\n",
FILE_APPEND);
}
private function _genDownPath() {
$date_format = date('YmdH', $_SERVER['REQUEST_TIME']);
$year = substr($date_format, 0, 4) . '/';
$month = substr($date_format, 4, 2) . '/';
$day = substr($date_format, 6, 2) . '/';
$hour = substr($date_format, 8) . '/';
// $dir = BASE_PATH . $this->getImgPath($img_type) . $year . $month . $day . $hour;
$dir = BASE_PATH . './downapp/' . $year . $month;
@mk_dir($dir);
return $dir;
}//end _genUpImgPath()
private function _updateHash($app_md5, $app_store) {
$app_md5 = trim($app_md5);
$app_store = trim($app_store);
$mApp = M('App');
$findAppId = $mApp ->where("app_md5 = '' AND app_store = '{$app_store}'")
->getField($mApp->getPk());
if($findAppId) {
$mApp->save(array(
$mApp->getPk() => intval($findAppId),
'app_md5' => $app_md5,
));
}
}
private function _finish_request($time = 1000) {
if(function_exists('fastcgi_finish_request')) {//如果服务跑的是 fastcgi 模式,就交给服务器自己去跑吧
fastcgi_finish_request();
}
set_time_limit($time);
}
}
?>
-------------------------------------------------------------------------------------------------------------------------------------------------------------------
啊!不太好意思,关于进程锁机制,这个程序不是我那个机制,因为当时只是想能让任务停止,所以只是检测到有锁时就终止任务,没有做到保证任务唯一性。也就是说只实现了一半的机制呵呵。。。。完整的机制其实就是反过来的:在程序开始时检测如果有锁就不执行任务,已经开始的任务先touch出锁文件来,然后每个循环开始时检测锁还在不在,不在就终止。一直到任务结束时再把锁文件删除掉。