文件下载限速
第一步.我们写一段使用 PHP 输出文件给浏览器下载的代码
<?php $filePath = './test.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; } fclose($fp); ?>
可以看出,PHP 实现浏览器下载文件,主要是靠 header 的支持以及 echo 文件数据,那么,如何限制速度呢?可以通过限制输出频率吗?
例如每次读取 1024 字节后就暂停一次.
<?php $filePath = './test.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 $filePath = './test.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);
我们可以增加下载速度,把 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 $filePath = './test.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 $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)
多线程下载
通过前面,我们或许发现了什么:
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 $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 秒完成