php文件下载限速,文件断点续传,多线程下载文件原理解析

文件下载限速

首先,我们写一段使用php输出文件给浏览器下载的代码

<?php
/**
 * Created by PhpStorm.
 * User: tioncico
 * Date: 19-2-4
 * Time: 下午4:30
 */
$filePath = './hyxd.zip';//文件
$fp=fopen($filePath,"r");

//取得文件大小
$fileSize=filesize($filePath);

header("Content-type:application/octet-stream");//设定header头为下载
header("Accept-Ranges:bytes");
header("Accept-Length:".$fileSize);//响应大小
header("Content-Disposition: attachment; filename=testNaame");//文件名

$buffer=1024;
$bufferCount=0;

while(!feof($fp)&&$fileSize-$bufferCount>0){//循环读取文件数据
    $data=fread($fp,$buffer);
    $bufferCount+=$buffer;
    echo $data;//输出文件
}

fclose($fp);

可以看出,php实现浏览器下载文件,主要是靠header头的支持以及echo 文件数据,那么,该如何限制速度呢?可以通过限制输出频率吗?例如每次读取1024之后,就进行一次sleep?

<?php
/**
 * Created by PhpStorm.
 * User: tioncico
 * Date: 19-2-4
 * Time: 下午4:30
 */
$filePath = './hyxd.zip';//文件
$fp=fopen($filePath,"r");

//取得文件大小
$fileSize=filesize($filePath);

header("Content-type:application/octet-stream");//设定header头为下载
header("Accept-Ranges:bytes");
header("Accept-Length:".$fileSize);//响应大小
header("Content-Disposition: attachment; filename=testName");//文件名

$buffer=1024;
$bufferCount=0;

while(!feof($fp)&&$fileSize-$bufferCount>0){//循环读取文件数据
    $data=fread($fp,$buffer);
    $bufferCount+=$buffer;
    echo $data;//输出文件
    sleep(1);//增加了一个sleep
}

fclose($fp);

但是通过浏览器访问,我们发现是不行的,甚至造成了浏览器只有在n秒之后才会出现下载确认框,是哪里出了问题呢?

其实,这是因为php的buffer引起的,php buffer缓冲区,会使php不会马上输出数据,而是需要等缓冲区满之后才会响应到web服务器,通过web服务器再响应到浏览器中,详细请看:关于php的buffer(缓冲区)

那该怎么改呢?其实很简单,只需要使用ob系列函数就可解决:

<?php
/**
 * Created by PhpStorm.
 * User: tioncico
 * Date: 19-2-4
 * Time: 下午4:30
 */
$filePath = './hyxd.zip';//文件
$fp=fopen($filePath,"r");

//取得文件大小
$fileSize=filesize($filePath);

header("Content-type:application/octet-stream");//设定header头为下载
header("Accept-Ranges:bytes");
header("Accept-Length:".$fileSize);//响应大小
header("Content-Disposition: attachment; filename=testName");//文件名
ob_end_clean();//缓冲区结束
ob_implicit_flush();//强制每当有输出的时候,即刻把输出发送到浏览器
header('X-Accel-Buffering: no'); // 不缓冲数据
$buffer=1024;
$bufferCount=0;

while(!feof($fp)&&$fileSize-$bufferCount>0){//循环读取文件数据
    $data=fread($fp,$buffer);
    $bufferCount+=$buffer;
    echo $data;//输出文件
    sleep(1);
}

fclose($fp);

这样,我们就已经实现了,每秒只输出1024字节的数据:

仙士可博客

我们可以增加下载速度,把buffer改成更大的值,例如102400,那么就会变成每秒下载100kb:

仙士可博客


文件断点续传

那么,我们该如何实现文件断点续传呢?首先,我们要了解http协议中,关于请求头的几个参数:

content-range和range,

在文件断点续传中,必须包含一个断点续传的参数,例如:

请求下载头:

Range: bytes=0-801 //一般请求下载整个文件是bytes=0- 或不用这个头

响应文件头:

Content-Range: bytes 0-800/801 //801:文件总大小


正常下载文件时,不需要使用range头,而当断点续传时,由于再之前已经获得了n字节数据,所以可以直接请求

Range: bytes=n字节-总文件大小,代表着n字节之前的数据不再下载


响应头也是如此,那么,我们通过之前的限速下载,进行暂停,然后继续下载试试吧:

仙士可博客

可看到,我们下载到600kb之后暂停了,然后我们代码记录下下次请求的请求数据:

<?php
/**
 * Created by PhpStorm.
 * User: tioncico
 * Date: 19-2-4
 * Time: 下午4:30
 */
$filePath = './hyxd.zip';//文件
$fp=fopen($filePath,"r");
set_time_limit(1);
//取得文件大小
$fileSize=filesize($filePath);
file_put_contents('1.txt',json_encode($_SERVER));
//下面的代码直接忽略了,主要看server

当我点击继续下载时,浏览器会报出下载失败,原因是我们没有正确的响应它需要的数据,然后我们看下1.txt并打印成数组:

仙士可博客

仙士可博客


可看到,浏览器增加了一个range的请求头参数,想请求61400字节-文件尾的文件数据,那么,我们后端该如何处理呢?

我们只需要输出61400之后的文件内容即可

为了方便测试查看,我将文件改为了2.txt,内容如下:

仙士可博客

编写可断点续传代码:

<?php
/**
 * Created by PhpStorm.
 * User: tioncico
 * Date: 19-2-4
 * Time: 下午4:30
 */
$filePath = './2.txt';//文件
$fp=fopen($filePath,"r");
//set_time_limit(1);
//取得文件大小
$fileSize=filesize($filePath);
$buffer=5000;
$bufferCount=0;
header("Content-type:application/octet-stream");//设定header头为下载
header("Content-Disposition: attachment; filename=2.txt");//文件名
if (!empty($_SERVER['HTTP_RANGE'])){
    //切割字符串
    $range = explode('-',substr($_SERVER['HTTP_RANGE'],6));
    fseek($fp,$range[0]);//移动文件指针到range上
    header('HTTP/1.1 206 Partial Content');
    header("Content-Range: bytes $range[0]-$fileSize/$fileSize");
    header("content-length:".$fileSize-$range[0]);
}else{
    header("Accept-Length:".$fileSize);//响应大小
}

ob_end_clean();//缓冲区结束
ob_implicit_flush();//强制每当有输出的时候,即刻把输出发送到浏览器
header('X-Accel-Buffering: no'); // 不缓冲数据
while(!feof($fp)&&$fileSize-$bufferCount>0){//循环读取文件数据
    $data=fread($fp,$buffer);
    $bufferCount+=$buffer;
    echo $data;//输出文件
    sleep(1);
}
fclose($fp);

使用谷歌浏览器进行下载并暂停

仙士可博客


查看当前下载内容:

仙士可博客



可看到,最后下载到的字符串为13517x,恢复浏览器下载,继续暂停

仙士可博客


仙士可博客

仙士可博客

成功对接,并看到现在断点在51017x中,继续下载直到完成:

使用代码验证:

$txt = file_get_contents('/home/tioncico/Downloads/2.txt');

$arr = explode('x',$txt);
var_dump(count($arr));
var_dump($arr[count($arr)-2]);

仙士可博客

成功下载



多线程下载

通过前面,我们或许发现了什么:

1:限速是限制当前连接的数量

2:可以通过range来实现文件分片下载


那么,我们能不能使用多个连接,每个连接只下载x个字节,到最后进行拼装成一个文件呢?答案是可以的

下面,我们就使用php的curl_multi进行多线程下载

<?php

$filePath = '127.0.0.1/2.txt';
//查看文件大小
$ch = curl_init();

//$headerData = [
//    "Range: bytes=0-1"
//];
//curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "HEAD");
curl_setopt($ch, CURLOPT_URL, $filePath);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print
curl_setopt($ch, CURLOPT_TIMEOUT, 30);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect
curl_setopt($ch, CURLOPT_MAXREDIRS, 7);
curl_setopt($ch, CURLOPT_HEADER, true);//需要获取header头
curl_setopt($ch, CURLOPT_NOBODY, 1);    //不需要body,只需要获取header头的文件大小
$sContent = curl_exec($ch);
// 获得响应结果里的:头大小
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);//获取header头大小
// 根据头大小去获取头信息内容
$header = substr($sContent, 0, $headerSize);//获取真实的header头
curl_close($ch);
$headerArr = explode("\r\n", $header);
foreach ($headerArr as $item) {
    $value = explode(':', $item);
    if ($value[0] == 'Content-Length') {
        $fileSize = (int)$value[1];//文件大小
        break;
    }
}

//开启多线程下载
$mh = curl_multi_init();
$count = 5;//n个线程
$handle = [];//n线程数组
$data = [];//数据分段数组
$fileData = ceil($fileSize / $count);
for ($i = 0; $i < $count; $i++) {
    $ch = curl_init();

    //判断是否读取数量大于剩余数量
    if ($fileData > ($fileSize-($i * $fileData))) {
        $headerData = [
            "Range:bytes=" . $i * $fileData . "-" . ($fileSize)
        ];
    }else{
        $headerData = [
            "Range:bytes=" . $i * $fileData . "-" .(($i+1)*$fileData)
        ];
    }
    echo PHP_EOL;
    curl_setopt($ch, CURLOPT_URL, $filePath);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print
    curl_setopt($ch, CURLOPT_TIMEOUT, 30);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);
    curl_setopt($ch, CURLOPT_MAXREDIRS, 7);
    curl_multi_add_handle($mh, $ch); // 把 curl resource 放进 multi curl handler 里
    $handle[$i] = $ch;
}
$active = null;

do {
    //同时执行多线程,直到全部完成或超时
    $mrc = curl_multi_exec($mh, $active);
} while ($active);

for ($i = 0; $i < $count; $i++) {
    $data[$i] = curl_multi_getcontent($handle[$i]);
    curl_multi_remove_handle($mh, $handle[$i]);
}
curl_multi_close($mh);
$file = implode('',$data);//组合成一个文件
$arr = explode('x',$file);
var_dump($data);
var_dump(count($arr));
var_dump($arr[count($arr)-2]);
//测试文件是否正确

运行截图:

仙士可博客

该代码将会开出5个线程,按照不同的文件段去同时下载,再最后组装成一个字符串,即实现了多线程下载

以上代码是访问nginx直接测试的,之前的代码不支持head  http头,我们需要修改一下才可以支持(但这是标准http写法)


我们需要修改下之前的代码,使其支持range的结束位置:

<?php
/**
 * Created by PhpStorm.
 * User: tioncico
 * Date: 19-2-4
 * Time: 下午4:30
 */
$filePath = './2.txt';//文件
$fp = fopen($filePath, "r");
//set_time_limit(1);
//取得文件大小
$fileSize = filesize($filePath);
$buffer = 50000;
$bufferCount = 0;
header("Content-type:application/octet-stream");//设定header头为下载
header("Content-Disposition: attachment; filename=2.txt");//文件名
if (!empty($_SERVER['HTTP_RANGE'])) {
    //切割字符串
    $range = explode('-', substr($_SERVER['HTTP_RANGE'], 6));
    fseek($fp, $range[0]);//移动文件指针到range上
    header('HTTP/1.1 206 Partial Content');
    header("Content-Range: bytes $range[0]-$range[1]/$fileSize");
    $range[1]>0&&$fileSize=$range[1];//只获取range[1]的数量
    header("content-length:" . $fileSize - $range[0]);
} else {
    header("Accept-Length:" . $fileSize);//响应大小
}

ob_end_clean();//缓冲区结束
ob_implicit_flush();//强制每当有输出的时候,即刻把输出发送到浏览器
header('X-Accel-Buffering: no'); // 不缓冲数据
while (!feof($fp) && $fileSize-$range[0] - $bufferCount > 0) {//循环读取文件数据
    //避免多读取
    $buffer>($fileSize-$range[0]-$bufferCount)&&$buffer=$fileSize-$range[0]-$bufferCount;
    $data = fread($fp, $buffer);
    $bufferCount += $buffer;
    echo $data;//输出文件
    sleep(1);
}
fclose($fp);

修改下多线程下载代码:

<?php

$filePath = '127.0.0.1';
//查看文件大小
$ch = curl_init();

$headerData = [
    "Range: bytes=0-1"
];
curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);
//curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "HEAD");
curl_setopt($ch, CURLOPT_URL, $filePath);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print
curl_setopt($ch, CURLOPT_TIMEOUT, 0);
curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect
curl_setopt($ch, CURLOPT_MAXREDIRS, 7);
curl_setopt($ch, CURLOPT_HEADER, true);//需要获取header头
curl_setopt($ch, CURLOPT_NOBODY, 1);    //不需要body,只需要获取header头的文件大小
$sContent = curl_exec($ch);
// 获得响应结果里的:头大小
$headerSize = curl_getinfo($ch, CURLINFO_HEADER_SIZE);//获取header头大小
// 根据头大小去获取头信息内容
$header = substr($sContent, 0, $headerSize);//获取真实的header头
curl_close($ch);
$headerArr = explode("\r\n", $header);
foreach ($headerArr as $item) {
    $value = explode(':', $item);
    if ($value[0] == 'Content-Range') {//通过分段,获取到文件大小
        $fileSize = explode('/',$value[1])[1];//文件大小
        break;
    }
}
var_dump($fileSize);
//开启多线程下载
$mh = curl_multi_init();
$count = 5;//n个线程
$handle = [];//n线程数组
$data = [];//数据分段数组
$fileData = ceil($fileSize / $count);
for ($i = 0; $i < $count; $i++) {
    $ch = curl_init();

    //判断是否读取数量大于剩余数量
    if ($fileData > ($fileSize-($i * $fileData))) {
        $headerData = [
            "Range:bytes=" . $i * $fileData . "-" . ($fileSize)
        ];
    }else{
        $headerData = [
            "Range:bytes=" . $i * $fileData . "-" .(($i+1)*$fileData)
        ];
    }
    echo PHP_EOL;
    curl_setopt($ch, CURLOPT_URL, $filePath);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); // return don't print
    curl_setopt($ch, CURLOPT_TIMEOUT, 0);
    curl_setopt($ch, CURLOPT_USERAGENT, 'Mozilla/4.0 (compatible; MSIE 5.01; Windows NT 5.0)');
    curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1); // 302 redirect
    curl_setopt($ch, CURLOPT_HTTPHEADER, $headerData);
    curl_setopt($ch, CURLOPT_MAXREDIRS, 7);
    curl_multi_add_handle($mh, $ch); // 把 curl resource 放进 multi curl handler 里
    $handle[$i] = $ch;
}
$active = null;

do {
    //同时执行多线程,直到全部完成或超时
    $mrc = curl_multi_exec($mh, $active);
} while ($active);

for ($i = 0; $i < $count; $i++) {
    $data[$i] = curl_multi_getcontent($handle[$i]);
    curl_multi_remove_handle($mh, $handle[$i]);
}
curl_multi_close($mh);
$file = implode('',$data);//组合成一个文件
$arr = explode('x',$file);
var_dump($data);
var_dump(count($arr));
var_dump($arr[count($arr)-2]);
//测试文件是否正确


仙士可博客


成功下载,测试耗时结果为:5个线程4秒左右完成,1个线程花费13秒完成

仙士可博客

仙士可博客


仙士可博客
请先登录后发表评论
  • 最新评论
  • 总共1条评论
仙士可博客

x:仙士可为什么这么牛逼

2019-02-04 23:00:15 回复

  • 本站由白俊遥博客程序搭建
    © 2017-1-17 php20.cn 版权所有 ICP证:闽ICP备17001387号
  • 联系邮箱:1067197739@qq.com