本文目录导读:

在PHP中实现断点续传下载,主要依赖于HTTP协议的Range头,以下是完整实现方案:
基础断点续传实现
<?php
function downloadFile($filePath, $fileName = '') {
// 验证文件
if (!file_exists($filePath)) {
http_response_code(404);
die('文件不存在');
}
// 获取文件信息
$fileSize = filesize($filePath);
$fileName = $fileName ?: basename($filePath);
// 设置基础头信息
header('Content-Type: application/octet-stream');
header('Content-Disposition: attachment; filename="' . $fileName . '"');
header('Accept-Ranges: bytes');
header('Content-Length: ' . $fileSize);
// 解析Range头
$start = 0;
$end = $fileSize - 1;
$isPartialRequest = false;
if (isset($_SERVER['HTTP_RANGE'])) {
// 只处理单个范围的请求
if (preg_match('/bytes=(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $matches)) {
$isPartialRequest = true;
if (!empty($matches[1])) {
$start = intval($matches[1]);
}
if (!empty($matches[2])) {
$end = intval($matches[2]);
}
// 验证范围有效性
if ($start >= $fileSize || $end >= $fileSize) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header('Content-Range: bytes */' . $fileSize);
exit;
}
if ($start > $end) {
header('HTTP/1.1 400 Bad Request');
exit;
}
}
}
// 设置响应状态码
if ($isPartialRequest) {
http_response_code(206);
header('Content-Range: bytes ' . $start . '-' . $end . '/' . $fileSize);
header('Content-Length: ' . ($end - $start + 1));
}
// 输出文件内容
$fp = fopen($filePath, 'rb');
if ($fp === false) {
http_response_code(500);
die('无法打开文件');
}
// 移动到指定位置
fseek($fp, $start);
// 设置缓冲大小
$buffer = 1024 * 1024; // 1MB
while (!feof($fp) && ($pos = ftell($fp)) <= $end) {
if ($pos + $buffer > $end) {
$buffer = $end - $pos + 1;
}
echo fread($fp, $buffer);
flush();
// 防止内存溢出
if (connection_aborted()) {
break;
}
}
fclose($fp);
exit;
}
支持多范围和特殊情况的增强版
<?php
class ResumeDownload {
private $filePath;
private $fileName;
private $mimeType;
public function __construct($filePath, $fileName = '', $mimeType = '') {
$this->filePath = $filePath;
$this->fileName = $fileName ?: basename($filePath);
$this->mimeType = $mimeType ?: $this->getMimeType($filePath);
}
public function download() {
// 检查文件
if (!file_exists($this->filePath)) {
$this->sendError(404, 'File not found');
return;
}
$fileSize = filesize($this->filePath);
// 处理Range请求
$ranges = $this->parseRanges();
header('Content-Type: ' . $this->mimeType);
header('Content-Disposition: attachment; filename="' . $this->fileName . '"');
header('Accept-Ranges: bytes');
header('Cache-Control: public, must-revalidate, max-age=0');
header('Pragma: public');
if (empty($ranges)) {
// 完整下载
header('Content-Length: ' . $fileSize);
$this->sendFileContents(0, $fileSize - 1);
} else {
// 多范围请求(简化处理,实际可支持多个范围)
$range = $ranges[0];
// 处理无效范围
if ($range[0] >= $fileSize || $range[1] >= $fileSize) {
header('HTTP/1.1 416 Requested Range Not Satisfiable');
header('Content-Length: 0');
header('Content-Range: bytes */' . $fileSize);
return;
}
// 发送206响应
header('HTTP/1.1 206 Partial Content');
header('Content-Range: bytes ' . $range[0] . '-' . $range[1] . '/' . $fileSize);
header('Content-Length: ' . ($range[1] - $range[0] + 1));
$this->sendFileContents($range[0], $range[1]);
}
}
private function parseRanges() {
if (!isset($_SERVER['HTTP_RANGE'])) {
return [];
}
$rangeHeader = $_SERVER['HTTP_RANGE'];
// 检查是否为bytes范围
if (strpos($rangeHeader, 'bytes') !== 0) {
return [];
}
// 解析范围
$ranges = [];
$parts = explode(',', substr($rangeHeader, 6));
foreach ($parts as $part) {
if (preg_match('/(\d*)-(\d*)/', trim($part), $matches)) {
$start = !empty($matches[1]) ? intval($matches[1]) : null;
$end = !empty($matches[2]) ? intval($matches[2]) : null;
if ($start === null) {
// 最后N个字节: -N
$fileSize = filesize($this->filePath);
$start = max(0, $fileSize - $end);
$end = $fileSize - 1;
} elseif ($end === null) {
// 从某位置到末尾: N-
$end = filesize($this->filePath) - 1;
}
$ranges[] = [$start, $end];
}
}
return $ranges;
}
private function sendFileContents($start, $end) {
$fp = fopen($this->filePath, 'rb');
if (!$fp) {
$this->sendError(500, 'Unable to open file');
return;
}
fseek($fp, $start);
$bytesToSend = $end - $start + 1;
$buffer = 8192; // 8KB
while ($bytesToSend > 0 && !feof($fp)) {
$read = min($buffer, $bytesToSend);
$data = fread($fp, $read);
if ($data === false) {
break;
}
echo $data;
flush();
$bytesToSend -= strlen($data);
// 检查连接
if (connection_aborted()) {
break;
}
}
fclose($fp);
}
private function sendError($code, $message) {
http_response_code($code);
echo $message;
}
private function getMimeType($filePath) {
$mimeTypes = [
'mp4' => 'video/mp4',
'avi' => 'video/x-msvideo',
'mp3' => 'audio/mpeg',
'pdf' => 'application/pdf',
'txt' => 'text/plain',
// 添加更多类型...
];
$ext = strtolower(pathinfo($filePath, PATHINFO_EXTENSION));
return isset($mimeTypes[$ext]) ? $mimeTypes[$ext] : 'application/octet-stream';
}
}
使用示例
<?php
// 简单用法
$filePath = '/path/to/file.zip';
downloadFile($filePath);
// 增强版用法
$download = new ResumeDownload('/path/to/video.mp4', 'myvideo.mp4');
$download->download();
// 直接在HTML中调用
<a href="download.php?file=bigfile.zip">下载文件</a>
前端配合实现
// 前端断点续传下载
async function downloadWithResume(url) {
try {
const response = await fetch(url, {
headers: {
'Range': 'bytes=0-1023' // 先下载第一部分
}
});
if (response.status === 206) {
const contentLength = response.headers.get('Content-Length');
const contentRange = response.headers.get('Content-Range');
console.log('Partial:', contentRange);
// 获取文件总大小
const totalSize = parseInt(contentRange.split('/')[1]);
// 继续下载剩余部分
let start = 1024;
while (start < totalSize) {
const nextResponse = await fetch(url, {
headers: {
'Range': `bytes=${start}-${start + 1023}`
}
});
await nextResponse.blob();
start += 1024;
}
}
} catch (error) {
console.error('Download error:', error);
}
}
重要注意事项
-
性能优化:
- 使用合适的内存缓冲大小(建议1-8MB)
- 使用
flush()及时输出
-
安全性:
- 验证文件路径,防止目录遍历攻击
- 检查用户权限
-
异常处理:
- 处理文件不存在、权限不足等情况
- 处理连接中断
-
兼容性:
- 支持IE、Chrome、Firefox等主流浏览器
- 支持curl、wget等下载工具
这个实现可以完美支持迅雷、IDM等下载工具的断点续传功能。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播