本文目录导读:

在PHP中实现大文件下载的断点续传,核心是利用HTTP协议的Range请求头和Content-Range响应头,以下是完整的实现方案:
基本断点续传实现
<?php
function downloadFile($filePath) {
if (!file_exists($filePath)) {
header("HTTP/1.1 404 Not Found");
exit;
}
$fileSize = filesize($filePath);
$fileName = basename($filePath);
// 设置响应头
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
header("Accept-Ranges: bytes");
header("Content-Length: {$fileSize}");
// 处理断点续传
if (isset($_SERVER['HTTP_RANGE'])) {
$range = $_SERVER['HTTP_RANGE'];
// 解析Range头
preg_match('/bytes=(\d+)-(\d*)/', $range, $matches);
$startPos = intval($matches[1]);
$endPos = $matches[2] === '' ? $fileSize - 1 : intval($matches[2]);
// 验证范围有效性
if ($startPos > $endPos || $startPos >= $fileSize) {
header("HTTP/1.1 416 Range Not Satisfiable");
header("Content-Range: bytes */{$fileSize}");
exit;
}
// 设置206 Partial Content状态码
header("HTTP/1.1 206 Partial Content");
header("Content-Range: bytes {$startPos}-{$endPos}/{$fileSize}");
header("Content-Length: " . ($endPos - $startPos + 1));
header("Cache-Control: no-cache");
// 输出文件内容
$fp = fopen($filePath, 'rb');
fseek($fp, $startPos);
$bytesToSend = $endPos - $startPos + 1;
$chunkSize = 8192; // 8KB chunks
while ($bytesToSend > 0 && !feof($fp)) {
$readSize = min($chunkSize, $bytesToSend);
echo fread($fp, $readSize);
$bytesToSend -= $readSize;
flush();
ob_flush();
}
fclose($fp);
} else {
// 完整文件下载
readfile($filePath);
}
exit;
}
// 使用示例
$filePath = '/path/to/large/file.zip';
downloadFile($filePath);
?>
增强版(支持重试、多线程)
<?php
class DownloadManager {
private $filePath;
private $fileName;
private $fileSize;
public function __construct($filePath) {
$this->filePath = $filePath;
$this->fileName = basename($filePath);
$this->fileSize = filesize($filePath);
}
public function handleRequest() {
if (!file_exists($this->filePath)) {
header("HTTP/1.1 404 Not Found");
exit("File not found");
}
$this->setHeaders();
if (isset($_SERVER['HTTP_RANGE'])) {
$this->handleRangeRequest();
} else {
$this->handleFullDownload();
}
}
private function setHeaders() {
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$this->fileName}\"");
header("Accept-Ranges: bytes");
header("Cache-Control: no-store, no-cache, must-revalidate");
header("Expires: 0");
header("Pragma: no-cache");
}
private function handleRangeRequest() {
$range = $_SERVER['HTTP_RANGE'];
if (!preg_match('/bytes=(\d+)-(\d*)/', $range, $matches)) {
header("HTTP/1.1 400 Bad Request");
exit("Invalid Range header");
}
$startPos = intval($matches[1]);
$endPos = $matches[2] === '' ? $this->fileSize - 1 : intval($matches[2]);
// 验证范围
if ($startPos > $endPos || $startPos >= $this->fileSize) {
header("HTTP/1.1 416 Range Not Satisfiable");
header("Content-Range: bytes */{$this->fileSize}");
exit;
}
// 设置206状态码
header("HTTP/1.1 206 Partial Content");
header("Content-Range: bytes {$startPos}-{$endPos}/{$this->fileSize}");
header("Content-Length: " . ($endPos - $startPos + 1));
// 输出文件
$this->outputFile($startPos, $endPos);
}
private function handleFullDownload() {
header("Content-Length: {$this->fileSize}");
$this->outputFile(0, $this->fileSize - 1);
}
private function outputFile($startPos, $endPos) {
$fp = fopen($this->filePath, 'rb');
if (!$fp) {
header("HTTP/1.1 500 Internal Server Error");
exit("Unable to open file");
}
fseek($fp, $startPos);
$bytesRemaining = $endPos - $startPos + 1;
$bufferSize = 8192; // 8KB
set_time_limit(0); // 取消脚本执行时间限制
while ($bytesRemaining > 0 && !feof($fp)) {
$readSize = min($bufferSize, $bytesRemaining);
echo fread($fp, $readSize);
$bytesRemaining -= $readSize;
// 刷新输出缓冲区
if (ob_get_level()) {
ob_flush();
}
flush();
// 防止内存溢出
if (connection_aborted()) {
break;
}
}
fclose($fp);
}
}
// 使用示例
$downloader = new DownloadManager('/path/to/large/file.mkv');
$downloader->handleRequest();
?>
客户端断点续传(JavaScript)
class ResumeDownload {
constructor(url, outputStream) {
this.url = url;
this.outputStream = outputStream;
this.downloadedBytes = 0;
this.abortController = new AbortController();
}
async start() {
try {
const response = await this.downloadChunk();
await this.processResponse(response);
} catch (error) {
if (error.name === 'AbortError') {
console.log('Download cancelled');
} else {
console.error('Download failed:', error);
// 尝试恢复
await this.resume();
}
}
}
async downloadChunk(startPos = 0) {
const headers = {};
if (startPos > 0) {
headers['Range'] = `bytes=${startPos}-`;
}
return fetch(this.url, {
headers: headers,
signal: this.abortController.signal
});
}
async processResponse(response) {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const contentLength = response.headers.get('content-length');
const totalSize = parseInt(contentLength) + this.downloadedBytes;
const reader = response.body.getReader();
const decoder = new TextDecoder();
while (true) {
const { done, value } = await reader.read();
if (done) {
console.log('Download complete');
break;
}
this.downloadedBytes += value.length;
const progress = ((this.downloadedBytes / totalSize) * 100).toFixed(2);
// 写入到输出流
this.outputStream.write(value);
// 更新进度
this.onProgress(progress);
}
}
onProgress(percent) {
console.log(`Download progress: ${percent}%`);
}
async resume() {
// 从上次下载位置继续
await this.downloadChunk(this.downloadedBytes);
}
cancel() {
this.abortController.abort();
}
}
// 使用示例
async function downloadLargeFile() {
const downloader = new ResumeDownload(
'http://example.com/large-file.zip',
new WritableStream()
);
try {
await downloader.start();
} catch (error) {
console.error('Download failed:', error);
}
}
性能优化建议
使用X-Sendfile(推荐)
// 使用Apache mod_xsendfile
header("X-Sendfile: {$filePath}");
header("Content-Type: application/octet-stream");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
// 使用Nginx X-Accel-Redirect
header("X-Accel-Redirect: /download/{$fileName}");
header("Content-Disposition: attachment; filename=\"{$fileName}\"");
数据库下载记录
// 记录下载进度到数据库
class DownloadTracker {
private $db;
public function saveProgress($fileId, $downloadedBytes) {
$stmt = $this->db->prepare("UPDATE download_progress
SET downloaded_bytes = ?
WHERE file_id = ?");
$stmt->execute([$downloadedBytes, $fileId]);
}
public function getProgress($fileId) {
$stmt = $this->db->prepare("SELECT downloaded_bytes
FROM download_progress
WHERE file_id = ?");
$stmt->execute([$fileId]);
return $stmt->fetchColumn() ?: 0;
}
}
注意事项
- 内存管理:使用chunk方式读取,避免一次性加载大文件到内存
- 超时设置:取消脚本执行时间限制
set_time_limit(0) - 错误处理:处理连接中断、范围无效等情况
- 缓存头:设置正确的Cache-Control头,防止代理缓存
- 会话锁:使用
session_write_close()避免会话锁
这个实现支持标准的断点续传功能,也兼容多线程下载工具(如IDM、迅雷等)的并发请求。