基于PHP实现类似百度网盘文件下载限速的功能
/**
* 对外输出文件
* @param string $file 文件相对路径或绝对路径
* @param string $name 文件名称(客户端浏览器显示使用)
* @param int $speed 下载速度(b/秒)
*/
function echoFile($file,$name,$speed){
set_time_limit(0);
ob_flush();
flush();
$fileSize = filesize($file);
header('Cache-Control: no-cache');
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . urlencode($name) . '"');
header('Accept-Ranges: bytes');
header("Date: ".gmdate("D, d M Y H:i:s",time())." GMT");
header("Last-Modified: " . gmdate("D, d M Y H:i:s",filemtime($file)) . " GMT");
$isRange = isset($_SERVER['HTTP_RANGE']);
$rangeList = analysisHttpRange($fileSize,$_SERVER['HTTP_RANGE']??null);
if(!$rangeList){
$rangeList = [
[
"start" => 0,
"end" => $fileSize - 1
]
];
}
$contentLength = 0;
foreach ($rangeList as $item) {
$contentLength += $item["end"] - $item["start"] + 1;
}
header('Content-Length: ' . $contentLength);
if($isRange){
header('HTTP/1.1 206 Partial Content');
$rangeStr = "";
foreach ($rangeList as $item) {
$str = $item["start"] . "-" . $item["end"] . "/" . $fileSize;
if($rangeStr == ""){
$rangeStr = $str;
}else{
$rangeStr = $rangeStr . "," . $str;
}
}
header("Content-Range: bytes " . $rangeStr);
}else{
header('HTTP/1.1 200 OK');
}
foreach ($rangeList as $item) {
$fp = fopen($file, 'r');
fseek($fp,$item["start"]);
$count = $item["end"] - $item["start"] + 1;
$sum = ceil($count / $speed);
for ($i=0; $i < $sum; $i++) {
if($i == $sum - 1){ // 最后一块文件
echo fread($fp, ($item["end"]+1) - (($sum - 1) * $speed));
}else{
echo fread($fp, $speed);
}
ob_flush();
flush();
sleep(1);
}
fclose($fp);
}
exit;
}
/**
* 解析http请求头中的range
*/
function analysisHttpRange(int $fileSize,$range):array{
if(!$range)return [];
$fileEnd = $fileSize - 1;
$range = preg_replace("/[^0-9\.\-\,]/", "", $range);
if(!$range)return [];
$rangeList = explode(",",$range);
if(count($rangeList) < 1)return [];
$list = [];
foreach ($rangeList as $item) {
$lineIndex = strpos($item,"-");
if($lineIndex === false)continue;
if($lineIndex === 0){ //反向获取
$end = (int)str_replace("-","",$item);
if($end > $fileEnd)$end = $fileEnd;
$list[] = [
"start" => $fileEnd - $end,
"end" => $fileEnd,
];
}else if($lineIndex == (strlen($item) - 1)){ //从指定位置到结束
$start = (int)str_replace("-","",$item);
if($start > $fileEnd)$start = $fileEnd;
$list[] = [
"start" => $start,
"end" => $fileEnd,
];
}else{ //选择区间
$se = explode("-",$item);
$se[0] = (int)$se[0];
$se[1] = (int)$se[1];
if(
$se[1] < $se[0]
|| $se[1] > $fileEnd
|| $se[0] > $fileEnd
)continue;
$list[] = [
"start" => $se[0],
"end" => $se[1]
];
}
}
$total = 0;
foreach ($list as $item) {
$total += $item["end"] - $item["start"];
}
if($total > $fileSize)return [];
return $list;
}
调用示例:
echoFile(
"./test.zip",
"测试.zip",
1024*1024*2
); //把test.zip以2m/s的速度发送到客户端
实现原理是在HTTP请求头中设置Range字段,指定需要下载的文件的起始字节和结束字节,服务器可以根据这个请求头返回相应的文件内容给客户端,客户端接收到的数据只是文件的一部分,而不是整个文件。客户端可以将这部分数据追加到已经下载的文件中,然后继续请求下一部分数据,直到完成整个文件的下载。