本文目录导读:

在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等下载工具的断点续传功能。